mirror of
https://github.com/lens-protocol/core.git
synced 2026-01-09 06:08:04 -05:00
Initial commit
This commit is contained in:
16
.eslintrc.json
Normal file
16
.eslintrc.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"env": { "node": true },
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"rules": {}
|
||||
}
|
||||
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
.env
|
||||
#Hardhat files
|
||||
cache
|
||||
artifacts
|
||||
node_modules
|
||||
dist/
|
||||
build/
|
||||
.vscode
|
||||
.idea
|
||||
/.VSCodeCounter/
|
||||
/types
|
||||
/typechain-types
|
||||
/tasks/unpause.ts
|
||||
/tasks/create-profile.ts
|
||||
/tasks/post.ts
|
||||
/tasks/follow.ts
|
||||
/tasks/collect.ts
|
||||
/tasks/create-module.ts
|
||||
/tasks/module-flow.ts
|
||||
|
||||
/coverage
|
||||
coverage.json
|
||||
.coverage_artifacts
|
||||
.coverage_cache
|
||||
.coverage_contracts
|
||||
|
||||
addresses.json
|
||||
5
.prettierignore
Normal file
5
.prettierignore
Normal file
@@ -0,0 +1,5 @@
|
||||
artifacts
|
||||
cache
|
||||
node_modules
|
||||
coverage
|
||||
typechain-types
|
||||
17
.prettierrc
Normal file
17
.prettierrc
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"trailingComma": "es5",
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.sol",
|
||||
"options": {
|
||||
"semi": true,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 4
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
8
.solcover.js
Normal file
8
.solcover.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
skipFiles: [
|
||||
'/core/base/ERC721Time.sol',
|
||||
'/core/base/ERC721Enumerable.sol',
|
||||
'/interfaces',
|
||||
'/mocks',
|
||||
],
|
||||
};
|
||||
5
Dockerfile
Normal file
5
Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
||||
FROM ethereum/solc:0.8.7 as build-deps
|
||||
|
||||
FROM node:16
|
||||
USER node
|
||||
COPY --from=build-deps /usr/bin/solc /usr/bin/solc
|
||||
182
README.md
Normal file
182
README.md
Normal file
@@ -0,0 +1,182 @@
|
||||
```
|
||||
_____ _____ _____ _____
|
||||
/\ \ /\ \ /\ \ /\ \
|
||||
/::\____\ /::\ \ /::\____\ /::\ \
|
||||
/:::/ / /::::\ \ /::::| | /::::\ \
|
||||
/:::/ / /::::::\ \ /:::::| | /::::::\ \
|
||||
/:::/ / /:::/\:::\ \ /::::::| | /:::/\:::\ \
|
||||
/:::/ / /:::/__\:::\ \ /:::/|::| | /:::/__\:::\ \
|
||||
/:::/ / /::::\ \:::\ \ /:::/ |::| | \:::\ \:::\ \
|
||||
/:::/ / /::::::\ \:::\ \ /:::/ |::| | _____ ___\:::\ \:::\ \
|
||||
/:::/ / /:::/\:::\ \:::\ \ /:::/ |::| |/\ \ /\ \:::\ \:::\ \
|
||||
/:::/____/ /:::/__\:::\ \:::\____\/:: / |::| /::\____\/::\ \:::\ \:::\____\
|
||||
\:::\ \ \:::\ \:::\ \::/ /\::/ /|::| /:::/ /\:::\ \:::\ \::/ /
|
||||
\:::\ \ \:::\ \:::\ \/____/ \/____/ |::| /:::/ / \:::\ \:::\ \/____/
|
||||
\:::\ \ \:::\ \:::\ \ |::|/:::/ / \:::\ \:::\ \
|
||||
\:::\ \ \:::\ \:::\____\ |::::::/ / \:::\ \:::\____\
|
||||
\:::\ \ \:::\ \::/ / |:::::/ / \:::\ /:::/ /
|
||||
\:::\ \ \:::\ \/____/ |::::/ / \:::\/:::/ /
|
||||
\:::\ \ \:::\ \ /:::/ / \::::::/ /
|
||||
\:::\____\ \:::\____\ /:::/ / \::::/ /
|
||||
\::/ / \::/ / \::/ / \::/ /
|
||||
\/____/ \/____/ \/____/ \/____/
|
||||
|
||||
|
||||
|
||||
```
|
||||
|
||||
# Lens Protocol
|
||||
|
||||
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.
|
||||
|
||||
## Setup
|
||||
|
||||
The environment is built using Docker Compose, note that your `.env` file must have an `ALCHEMY_KEY` or an `INFURA_KEY` variable, and an optional `MNEMONIC` and `ETHERSCAN_KEY`, defined like so:
|
||||
|
||||
```
|
||||
MNEMONIC="MNEMONIC YOU WANT TO DERIVE WALLETS FROM HERE"
|
||||
ALCHEMY_KEY="YOUR ALCHEMY KEY HERE"
|
||||
INFURA_KEY="OR YOUR INFURA KEY HERE"
|
||||
ETHERSCAN_KEY="YOUR ETHERSCAN API KEY HERE"
|
||||
```
|
||||
|
||||
With the environment file set up, you can move on to using Docker:
|
||||
|
||||
```
|
||||
$ sudo docker-compose up
|
||||
```
|
||||
|
||||
And in another terminal:
|
||||
|
||||
```
|
||||
$ sudo docker-compose exec contracts-env bash
|
||||
```
|
||||
|
||||
From there, have fun!
|
||||
|
||||
Here are a few self-explanatory scripts:
|
||||
|
||||
```
|
||||
$ npm run test
|
||||
$ npm run coverage
|
||||
$ npm run compile
|
||||
```
|
||||
|
||||
## 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 profileIdPointed The profile token ID this publication points to, for mirrors and comments.
|
||||
* @param pubIdPointed 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 profileIdPointed;
|
||||
uint256 pubIdPointed;
|
||||
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, contianing 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 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.
|
||||
76
contracts/core/CollectNFT.sol
Normal file
76
contracts/core/CollectNFT.sol
Normal file
@@ -0,0 +1,76 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {ICollectNFT} from '../interfaces/ICollectNFT.sol';
|
||||
import {ILensHub} from '../interfaces/ILensHub.sol';
|
||||
import {Errors} from '../libraries/Errors.sol';
|
||||
import {Events} from '../libraries/Events.sol';
|
||||
import {LensNFTBase} from './base/LensNFTBase.sol';
|
||||
|
||||
/**
|
||||
* @title CollectNFT
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This is the NFT contract that is minted upon collecting a given publication. It is cloned upon
|
||||
* the first collect for a given publication, and the token URI points to the original publication's contentURI.
|
||||
*/
|
||||
contract CollectNFT is ICollectNFT, LensNFTBase {
|
||||
address public immutable HUB;
|
||||
|
||||
uint256 internal _profileId;
|
||||
uint256 internal _pubId;
|
||||
uint256 internal _tokenIdCounter;
|
||||
|
||||
bool private _initialized;
|
||||
|
||||
// We create the CollectNFT with the pre-computed HUB address before deploying the hub proxy in order
|
||||
// to initialize the hub proxy at construction.
|
||||
constructor(address hub) {
|
||||
HUB = hub;
|
||||
}
|
||||
|
||||
/// @inheritdoc ICollectNFT
|
||||
function initialize(
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
string calldata name,
|
||||
string calldata symbol
|
||||
) external override {
|
||||
if (_initialized) revert Errors.Initialized();
|
||||
_initialized = true;
|
||||
_profileId = profileId;
|
||||
_pubId = pubId;
|
||||
super._initialize(name, symbol);
|
||||
emit Events.CollectNFTInitialized(profileId, pubId, block.timestamp);
|
||||
}
|
||||
|
||||
/// @inheritdoc ICollectNFT
|
||||
function mint(address to) external override {
|
||||
if (msg.sender != HUB) revert Errors.NotHub();
|
||||
uint256 tokenId = ++_tokenIdCounter;
|
||||
_mint(to, tokenId);
|
||||
}
|
||||
|
||||
/// @inheritdoc ICollectNFT
|
||||
function getSourcePublicationPointer() external view override returns (uint256, uint256) {
|
||||
return (_profileId, _pubId);
|
||||
}
|
||||
|
||||
function tokenURI(uint256 tokenId) public view override returns (string memory) {
|
||||
if (!_exists(tokenId)) revert Errors.TokenDoesNotExist();
|
||||
return ILensHub(HUB).getContentURI(_profileId, _pubId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Upon transfers, we emit the transfer event in the hub.
|
||||
*/
|
||||
function _beforeTokenTransfer(
|
||||
address from,
|
||||
address to,
|
||||
uint256 tokenId
|
||||
) internal override {
|
||||
super._beforeTokenTransfer(from, to, tokenId);
|
||||
ILensHub(HUB).emitCollectNFTTransferEvent(_profileId, _pubId, tokenId, from, to);
|
||||
}
|
||||
}
|
||||
224
contracts/core/FollowNFT.sol
Normal file
224
contracts/core/FollowNFT.sol
Normal file
@@ -0,0 +1,224 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {IFollowNFT} from '../interfaces/IFollowNFT.sol';
|
||||
import {IFollowModule} from '../interfaces/IFollowModule.sol';
|
||||
import {ILensHub} from '../interfaces/ILensHub.sol';
|
||||
import {Errors} from '../libraries/Errors.sol';
|
||||
import {Events} from '../libraries/Events.sol';
|
||||
import {DataTypes} from '../libraries/DataTypes.sol';
|
||||
import {LensNFTBase} from './base/LensNFTBase.sol';
|
||||
import {IERC721Metadata} from '@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol';
|
||||
|
||||
/**
|
||||
* @title FollowNFT
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This contract is the NFT that is minted upon following a given profile. It is cloned upon first follow for a
|
||||
* given profile, and includes built-in governance power and delegation mechanisms.
|
||||
*/
|
||||
contract FollowNFT is LensNFTBase, IFollowNFT {
|
||||
struct Snapshot {
|
||||
uint128 blockNumber;
|
||||
uint128 value;
|
||||
}
|
||||
|
||||
address public immutable HUB;
|
||||
|
||||
bytes32 internal constant DELEGATE_BY_SIG_TYPEHASH =
|
||||
0xb8f190a57772800093f4e2b186099eb4f1df0ed7f5e2791e89a4a07678e0aeff;
|
||||
// keccak256(
|
||||
// 'DelegateBySig(address delegator,address delegatee,uint256 nonce,uint256 deadline)'
|
||||
// );
|
||||
|
||||
mapping(address => mapping(uint256 => Snapshot)) internal _snapshots;
|
||||
mapping(address => address) internal _delegates;
|
||||
mapping(address => uint256) internal _snapshotCount;
|
||||
uint256 internal _profileId;
|
||||
uint256 internal _tokenIdCounter;
|
||||
|
||||
bool private _initialized;
|
||||
|
||||
// We create the FollowNFT with the pre-computed HUB address before deploying the hub.
|
||||
constructor(address hub) {
|
||||
HUB = hub;
|
||||
}
|
||||
|
||||
/// @inheritdoc IFollowNFT
|
||||
function initialize(
|
||||
uint256 profileId,
|
||||
string calldata name,
|
||||
string calldata symbol
|
||||
) external override {
|
||||
if (_initialized) revert Errors.Initialized();
|
||||
_initialized = true;
|
||||
_profileId = profileId;
|
||||
super._initialize(name, symbol);
|
||||
emit Events.FollowNFTInitialized(profileId, block.timestamp);
|
||||
}
|
||||
|
||||
/// @inheritdoc IFollowNFT
|
||||
function mint(address to) external override {
|
||||
if (msg.sender != HUB) revert Errors.NotHub();
|
||||
uint256 tokenId = ++_tokenIdCounter;
|
||||
_mint(to, tokenId);
|
||||
}
|
||||
|
||||
/// @inheritdoc IFollowNFT
|
||||
function delegate(address delegatee) external override {
|
||||
_delegate(msg.sender, delegatee);
|
||||
}
|
||||
|
||||
/// @inheritdoc IFollowNFT
|
||||
function delegateBySig(
|
||||
address delegator,
|
||||
address delegatee,
|
||||
DataTypes.EIP712Signature calldata sig
|
||||
) external override {
|
||||
bytes32 digest;
|
||||
unchecked {
|
||||
digest = keccak256(
|
||||
abi.encodePacked(
|
||||
'\x19\x01',
|
||||
_calculateDomainSeparator(),
|
||||
keccak256(
|
||||
abi.encode(
|
||||
DELEGATE_BY_SIG_TYPEHASH,
|
||||
delegator,
|
||||
delegatee,
|
||||
sigNonces[delegator]++,
|
||||
sig.deadline
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
_validateRecoveredAddress(digest, delegator, sig);
|
||||
_delegate(delegator, delegatee);
|
||||
}
|
||||
|
||||
/// @inheritdoc IFollowNFT
|
||||
function getPowerByBlockNumber(address user, uint256 blockNumber)
|
||||
external
|
||||
view
|
||||
override
|
||||
returns (uint256)
|
||||
{
|
||||
if (blockNumber > block.number) revert Errors.BlockNumberInvalid();
|
||||
|
||||
uint256 snapshotCount = _snapshotCount[user];
|
||||
|
||||
if (snapshotCount == 0) {
|
||||
return 0; //balanceOf(user); // Returning zero since this means the user never delegated and has no power
|
||||
}
|
||||
|
||||
uint256 lower = 0;
|
||||
uint256 upper = snapshotCount - 1;
|
||||
|
||||
// First check most recent balance
|
||||
if (_snapshots[user][upper].blockNumber <= blockNumber) {
|
||||
return _snapshots[user][upper].value;
|
||||
}
|
||||
|
||||
// Next check implicit zero balance
|
||||
if (_snapshots[user][lower].blockNumber > blockNumber) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
while (upper > lower) {
|
||||
uint256 center = upper - (upper - lower) / 2; // ceil, avoiding overflow
|
||||
Snapshot memory snapshot = _snapshots[user][center];
|
||||
if (snapshot.blockNumber == blockNumber) {
|
||||
return snapshot.value;
|
||||
} else if (snapshot.blockNumber < blockNumber) {
|
||||
lower = center;
|
||||
} else {
|
||||
upper = center - 1;
|
||||
}
|
||||
}
|
||||
return _snapshots[user][lower].value;
|
||||
}
|
||||
|
||||
function tokenURI(uint256 tokenId) public view override returns (string memory) {
|
||||
// NOTE: This is *temporary* and will change.
|
||||
if (!_exists(tokenId)) revert Errors.TokenDoesNotExist();
|
||||
return ILensHub(HUB).getFollowNFTURI(_profileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Upon transfers, we move the appropriate delegations, and emit the transfer event in the hub.
|
||||
*/
|
||||
function _beforeTokenTransfer(
|
||||
address from,
|
||||
address to,
|
||||
uint256 tokenId
|
||||
) internal override {
|
||||
address fromDelegatee = from != address(0) ? _delegates[from] : address(0);
|
||||
address toDelegatee = to != address(0) ? _delegates[to] : address(0);
|
||||
|
||||
address followModule = ILensHub(HUB).getFollowModule(_profileId);
|
||||
_moveDelegate(fromDelegatee, toDelegatee, 1);
|
||||
super._beforeTokenTransfer(from, to, tokenId);
|
||||
ILensHub(HUB).emitFollowNFTTransferEvent(_profileId, tokenId, from, to);
|
||||
if (followModule != address(0)) {
|
||||
IFollowModule(followModule).followModuleTransferHook(_profileId, from, to, tokenId);
|
||||
}
|
||||
}
|
||||
|
||||
function _delegate(address delegator, address delegatee) internal {
|
||||
uint256 delegatorBalance = balanceOf(delegator);
|
||||
address previousDelegate = _delegates[delegator];
|
||||
_delegates[delegator] = delegatee;
|
||||
_moveDelegate(previousDelegate, delegatee, delegatorBalance);
|
||||
}
|
||||
|
||||
function _moveDelegate(
|
||||
address from,
|
||||
address to,
|
||||
uint256 amount
|
||||
) internal {
|
||||
// NOTE: Since we start with no delegate, this condition is only fulfilled if a delegation occurred
|
||||
if (from != address(0)) {
|
||||
uint256 previous = 0;
|
||||
uint256 fromSnapshotCount = _snapshotCount[from];
|
||||
|
||||
previous = _snapshots[from][fromSnapshotCount - 1].value;
|
||||
|
||||
_writeSnapshot(from, uint128(previous - amount), fromSnapshotCount);
|
||||
emit Events.FollowNFTDelegatedPowerChanged(from, previous - amount, block.timestamp);
|
||||
}
|
||||
|
||||
if (to != address(0)) {
|
||||
uint256 previous = 0;
|
||||
uint256 toSnapshotCount = _snapshotCount[to];
|
||||
|
||||
if (toSnapshotCount != 0) {
|
||||
previous = _snapshots[to][toSnapshotCount - 1].value;
|
||||
}
|
||||
_writeSnapshot(to, uint128(previous + amount), toSnapshotCount);
|
||||
emit Events.FollowNFTDelegatedPowerChanged(to, previous + amount, block.timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
// Passing the snapshot count to prevent reading from storage to fetch it again in case of multiple operations
|
||||
function _writeSnapshot(
|
||||
address owner,
|
||||
uint128 newValue,
|
||||
uint256 ownerSnapshotCount
|
||||
) internal {
|
||||
uint128 currentBlock = uint128(block.number);
|
||||
mapping(uint256 => Snapshot) storage ownerSnapshots = _snapshots[owner];
|
||||
|
||||
// Doing multiple operations in the same block
|
||||
if (
|
||||
ownerSnapshotCount != 0 &&
|
||||
ownerSnapshots[ownerSnapshotCount - 1].blockNumber == currentBlock
|
||||
) {
|
||||
ownerSnapshots[ownerSnapshotCount - 1].value = newValue;
|
||||
} else {
|
||||
ownerSnapshots[ownerSnapshotCount] = Snapshot(currentBlock, newValue);
|
||||
_snapshotCount[owner] = ownerSnapshotCount + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
959
contracts/core/LensHub.sol
Normal file
959
contracts/core/LensHub.sol
Normal file
@@ -0,0 +1,959 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {ILensHub} from '../interfaces/ILensHub.sol';
|
||||
import {Events} from '../libraries/Events.sol';
|
||||
import {Helpers} from '../libraries/Helpers.sol';
|
||||
import {DataTypes} from '../libraries/DataTypes.sol';
|
||||
import {Errors} from '../libraries/Errors.sol';
|
||||
import {PublishingLogic} from '../libraries/PublishingLogic.sol';
|
||||
import {InteractionLogic} from '../libraries/InteractionLogic.sol';
|
||||
import {LensNFTBase} from './base/LensNFTBase.sol';
|
||||
import {LensMultiState} from './base/LensMultiState.sol';
|
||||
import {LensHubStorage} from './storage/LensHubStorage.sol';
|
||||
import {VersionedInitializable} from '../upgradeability/VersionedInitializable.sol';
|
||||
|
||||
/**
|
||||
* @title LensHub
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This is the main entrypoint of the Lens protocol. It contains governance functionality as well as
|
||||
* publishing and profile interaction functionality.
|
||||
*
|
||||
* NOTE: The Lens protocol is unique in that frontend operators need to track a potentially overwhelming
|
||||
* number of NFT contracts and interactions at once. For that reason, we've made two quirky design decisions:
|
||||
* 1. Both Follow & Collect NFTs invoke an LensHub callback on transfer with the sole purpose of emitting an event.
|
||||
* 2. Almost every event in the protocol emits the current block timestamp, reducing the need to fetch it manually.
|
||||
*/
|
||||
contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiState, LensHubStorage {
|
||||
uint256 internal constant REVISION = 1;
|
||||
|
||||
address internal immutable FOLLOW_NFT_IMPL;
|
||||
address internal immutable COLLECT_NFT_IMPL;
|
||||
|
||||
/**
|
||||
* @dev This modifier reverts if the caller is not the configured governance address.
|
||||
*/
|
||||
modifier onlyGov() {
|
||||
_validateCallerIsGovernance();
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev This modifier reverts if the caller is not a whitelisted profile creator address.
|
||||
*/
|
||||
modifier onlyWhitelistedProfileCreator() {
|
||||
_validateCallerIsWhitelistedProfileCreator();
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev The constructor sets the immutable follow & collect NFT implementations.
|
||||
*
|
||||
* @param followNFTImpl The follow NFT implementation address.
|
||||
* @param collectNFTImpl The collect NFT implementation address.
|
||||
*/
|
||||
constructor(address followNFTImpl, address collectNFTImpl) {
|
||||
FOLLOW_NFT_IMPL = followNFTImpl;
|
||||
COLLECT_NFT_IMPL = collectNFTImpl;
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function initialize(
|
||||
string calldata name,
|
||||
string calldata symbol,
|
||||
address newGovernance
|
||||
) external override initializer {
|
||||
super._initialize(name, symbol);
|
||||
_setState(DataTypes.ProtocolState.Paused);
|
||||
_setGovernance(newGovernance);
|
||||
}
|
||||
|
||||
/// ***********************
|
||||
/// *****GOV FUNCTIONS*****
|
||||
/// ***********************
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function setGovernance(address newGovernance) external override onlyGov {
|
||||
_setGovernance(newGovernance);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function setEmergencyAdmin(address newEmergencyAdmin) external override onlyGov {
|
||||
address prevEmergencyAdmin = _emergencyAdmin;
|
||||
_emergencyAdmin = newEmergencyAdmin;
|
||||
emit Events.EmergencyAdminSet(
|
||||
msg.sender,
|
||||
prevEmergencyAdmin,
|
||||
newEmergencyAdmin,
|
||||
block.timestamp
|
||||
);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function setState(DataTypes.ProtocolState newState) external override {
|
||||
if (msg.sender != _governance && msg.sender != _emergencyAdmin)
|
||||
revert Errors.NotGovernanceOrEmergencyAdmin();
|
||||
_setState(newState);
|
||||
}
|
||||
|
||||
///@inheritdoc ILensHub
|
||||
function whitelistProfileCreator(address profileCreator, bool whitelist)
|
||||
external
|
||||
override
|
||||
onlyGov
|
||||
{
|
||||
_profileCreatorWhitelisted[profileCreator] = whitelist;
|
||||
emit Events.ProfileCreatorWhitelisted(profileCreator, whitelist, block.timestamp);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function whitelistFollowModule(address followModule, bool whitelist) external override onlyGov {
|
||||
_followModuleWhitelisted[followModule] = whitelist;
|
||||
emit Events.FollowModuleWhitelisted(followModule, whitelist, block.timestamp);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function whitelistReferenceModule(address referenceModule, bool whitelist)
|
||||
external
|
||||
override
|
||||
onlyGov
|
||||
{
|
||||
_referenceModuleWhitelisted[referenceModule] = whitelist;
|
||||
emit Events.ReferenceModuleWhitelisted(referenceModule, whitelist, block.timestamp);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function whitelistCollectModule(address collectModule, bool whitelist)
|
||||
external
|
||||
override
|
||||
onlyGov
|
||||
{
|
||||
_collectModuleWhitelisted[collectModule] = whitelist;
|
||||
emit Events.CollectModuleWhitelisted(collectModule, whitelist, block.timestamp);
|
||||
}
|
||||
|
||||
/// *********************************
|
||||
/// *****PROFILE OWNER FUNCTIONS*****
|
||||
/// *********************************
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function createProfile(DataTypes.CreateProfileData calldata vars)
|
||||
external
|
||||
override
|
||||
whenNotPaused
|
||||
onlyWhitelistedProfileCreator
|
||||
{
|
||||
uint256 profileId = ++_profileCounter;
|
||||
_mint(vars.to, profileId);
|
||||
PublishingLogic.createProfile(
|
||||
vars,
|
||||
profileId,
|
||||
_profileIdByHandleHash,
|
||||
_profileById,
|
||||
_followModuleWhitelisted
|
||||
);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function setFollowModule(
|
||||
uint256 profileId,
|
||||
address followModule,
|
||||
bytes calldata followModuleData
|
||||
) external override whenNotPaused {
|
||||
_validateCallerIsProfileOwner(profileId);
|
||||
PublishingLogic.setFollowModule(
|
||||
profileId,
|
||||
followModule,
|
||||
followModuleData,
|
||||
_profileById[profileId],
|
||||
_followModuleWhitelisted
|
||||
);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function setFollowModuleWithSig(DataTypes.SetFollowModuleWithSigData calldata vars)
|
||||
external
|
||||
override
|
||||
whenNotPaused
|
||||
{
|
||||
address owner = ownerOf(vars.profileId);
|
||||
bytes32 digest;
|
||||
unchecked {
|
||||
digest = keccak256(
|
||||
abi.encodePacked(
|
||||
'\x19\x01',
|
||||
_calculateDomainSeparator(),
|
||||
keccak256(
|
||||
abi.encode(
|
||||
SET_FOLLOW_MODULE_WITH_SIG_TYPEHASH,
|
||||
vars.profileId,
|
||||
vars.followModule,
|
||||
keccak256(vars.followModuleData),
|
||||
sigNonces[owner]++,
|
||||
vars.sig.deadline
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
_validateRecoveredAddress(digest, owner, vars.sig);
|
||||
PublishingLogic.setFollowModule(
|
||||
vars.profileId,
|
||||
vars.followModule,
|
||||
vars.followModuleData,
|
||||
_profileById[vars.profileId],
|
||||
_followModuleWhitelisted
|
||||
);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function setDispatcher(uint256 profileId, address dispatcher) external override whenNotPaused {
|
||||
_validateCallerIsProfileOwner(profileId);
|
||||
_setDispatcher(profileId, dispatcher);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function setDispatcherWithSig(DataTypes.SetDispatcherWithSigData calldata vars)
|
||||
external
|
||||
override
|
||||
whenNotPaused
|
||||
{
|
||||
address owner = ownerOf(vars.profileId);
|
||||
bytes32 digest;
|
||||
unchecked {
|
||||
digest = keccak256(
|
||||
abi.encodePacked(
|
||||
'\x19\x01',
|
||||
_calculateDomainSeparator(),
|
||||
keccak256(
|
||||
abi.encode(
|
||||
SET_DISPATCHER_WITH_SIG_TYPEHASH,
|
||||
vars.profileId,
|
||||
vars.dispatcher,
|
||||
sigNonces[owner]++,
|
||||
vars.sig.deadline
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
_validateRecoveredAddress(digest, owner, vars.sig);
|
||||
_setDispatcher(vars.profileId, vars.dispatcher);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function setProfileImageURI(uint256 profileId, string calldata imageURI)
|
||||
external
|
||||
override
|
||||
whenNotPaused
|
||||
{
|
||||
_validateCallerIsProfileOwnerOrDispatcher(profileId);
|
||||
_setProfileImageURI(profileId, imageURI);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function setProfileImageURIWithSig(DataTypes.SetProfileImageURIWithSigData calldata vars)
|
||||
external
|
||||
override
|
||||
whenNotPaused
|
||||
{
|
||||
address owner = ownerOf(vars.profileId);
|
||||
bytes32 digest;
|
||||
unchecked {
|
||||
digest = keccak256(
|
||||
abi.encodePacked(
|
||||
'\x19\x01',
|
||||
_calculateDomainSeparator(),
|
||||
keccak256(
|
||||
abi.encode(
|
||||
SET_PROFILE_IMAGE_URI_WITH_SIG_TYPEHASH,
|
||||
vars.profileId,
|
||||
keccak256(bytes(vars.imageURI)),
|
||||
sigNonces[owner]++,
|
||||
vars.sig.deadline
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
_validateRecoveredAddress(digest, owner, vars.sig);
|
||||
_setProfileImageURI(vars.profileId, vars.imageURI);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function setFollowNFTURI(uint256 profileId, string calldata followNFTURI)
|
||||
external
|
||||
override
|
||||
whenNotPaused
|
||||
{
|
||||
_validateCallerIsProfileOwnerOrDispatcher(profileId);
|
||||
_setFollowNFTURI(profileId, followNFTURI);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function setFollowNFTURIWithSig(DataTypes.SetFollowNFTURIWithSigData calldata vars)
|
||||
external
|
||||
override
|
||||
whenNotPaused
|
||||
{
|
||||
address owner = ownerOf(vars.profileId);
|
||||
bytes32 digest;
|
||||
unchecked {
|
||||
digest = keccak256(
|
||||
abi.encodePacked(
|
||||
'\x19\x01',
|
||||
_calculateDomainSeparator(),
|
||||
keccak256(
|
||||
abi.encode(
|
||||
SET_FOLLOW_NFT_URI_WITH_SIG_TYPEHASH,
|
||||
vars.profileId,
|
||||
keccak256(bytes(vars.followNFTURI)),
|
||||
sigNonces[owner]++,
|
||||
vars.sig.deadline
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
_validateRecoveredAddress(digest, owner, vars.sig);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function post(DataTypes.PostData calldata vars) external override whenPublishingEnabled {
|
||||
_validateCallerIsProfileOwnerOrDispatcher(vars.profileId);
|
||||
_createPost(
|
||||
vars.profileId,
|
||||
vars.contentURI,
|
||||
vars.collectModule,
|
||||
vars.collectModuleData,
|
||||
vars.referenceModule,
|
||||
vars.referenceModuleData
|
||||
);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function postWithSig(DataTypes.PostWithSigData calldata vars)
|
||||
external
|
||||
override
|
||||
whenPublishingEnabled
|
||||
{
|
||||
address owner = ownerOf(vars.profileId);
|
||||
bytes32 digest;
|
||||
unchecked {
|
||||
digest = keccak256(
|
||||
abi.encodePacked(
|
||||
'\x19\x01',
|
||||
_calculateDomainSeparator(),
|
||||
keccak256(
|
||||
abi.encode(
|
||||
POST_WITH_SIG_TYPEHASH,
|
||||
vars.profileId,
|
||||
keccak256(bytes(vars.contentURI)),
|
||||
vars.collectModule,
|
||||
keccak256(vars.collectModuleData),
|
||||
vars.referenceModule,
|
||||
keccak256(vars.referenceModuleData),
|
||||
sigNonces[owner]++,
|
||||
vars.sig.deadline
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
_validateRecoveredAddress(digest, owner, vars.sig);
|
||||
_createPost(
|
||||
vars.profileId,
|
||||
vars.contentURI,
|
||||
vars.collectModule,
|
||||
vars.collectModuleData,
|
||||
vars.referenceModule,
|
||||
vars.referenceModuleData
|
||||
);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function comment(DataTypes.CommentData calldata vars) external override whenPublishingEnabled {
|
||||
_validateCallerIsProfileOwnerOrDispatcher(vars.profileId);
|
||||
_createComment(vars);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function commentWithSig(DataTypes.CommentWithSigData calldata vars)
|
||||
external
|
||||
override
|
||||
whenPublishingEnabled
|
||||
{
|
||||
address owner = ownerOf(vars.profileId);
|
||||
bytes32 digest;
|
||||
unchecked {
|
||||
digest = keccak256(
|
||||
abi.encodePacked(
|
||||
'\x19\x01',
|
||||
_calculateDomainSeparator(),
|
||||
keccak256(
|
||||
abi.encode(
|
||||
COMMENT_WITH_SIG_TYPEHASH,
|
||||
vars.profileId,
|
||||
keccak256(bytes(vars.contentURI)),
|
||||
vars.profileIdPointed,
|
||||
vars.pubIdPointed,
|
||||
vars.collectModule,
|
||||
keccak256(vars.collectModuleData),
|
||||
vars.referenceModule,
|
||||
keccak256(vars.referenceModuleData),
|
||||
sigNonces[owner]++,
|
||||
vars.sig.deadline
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
_validateRecoveredAddress(digest, owner, vars.sig);
|
||||
_createComment(
|
||||
DataTypes.CommentData(
|
||||
vars.profileId,
|
||||
vars.contentURI,
|
||||
vars.profileIdPointed,
|
||||
vars.pubIdPointed,
|
||||
vars.collectModule,
|
||||
vars.collectModuleData,
|
||||
vars.referenceModule,
|
||||
vars.referenceModuleData
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function mirror(DataTypes.MirrorData calldata vars) external override whenPublishingEnabled {
|
||||
_validateCallerIsProfileOwnerOrDispatcher(vars.profileId);
|
||||
_createMirror(
|
||||
vars.profileId,
|
||||
vars.profileIdPointed,
|
||||
vars.pubIdPointed,
|
||||
vars.referenceModule,
|
||||
vars.referenceModuleData
|
||||
);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function mirrorWithSig(DataTypes.MirrorWithSigData calldata vars)
|
||||
external
|
||||
override
|
||||
whenPublishingEnabled
|
||||
{
|
||||
address owner = ownerOf(vars.profileId);
|
||||
bytes32 digest;
|
||||
unchecked {
|
||||
digest = keccak256(
|
||||
abi.encodePacked(
|
||||
'\x19\x01',
|
||||
_calculateDomainSeparator(),
|
||||
keccak256(
|
||||
abi.encode(
|
||||
MIRROR_WITH_SIG_TYPEHASH,
|
||||
vars.profileId,
|
||||
vars.profileIdPointed,
|
||||
vars.pubIdPointed,
|
||||
vars.referenceModule,
|
||||
keccak256(vars.referenceModuleData),
|
||||
sigNonces[owner]++,
|
||||
vars.sig.deadline
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
_validateRecoveredAddress(digest, owner, vars.sig);
|
||||
_createMirror(
|
||||
vars.profileId,
|
||||
vars.profileIdPointed,
|
||||
vars.pubIdPointed,
|
||||
vars.referenceModule,
|
||||
vars.referenceModuleData
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Burns a profile, this maintains the profile data struct, but deletes the
|
||||
* handle hash to profile ID mapping value.
|
||||
*
|
||||
* NOTE: This overrides the LensNFTBase contract's `burn()` function and calls it to fully burn
|
||||
* the NFT.
|
||||
*/
|
||||
function burn(uint256 tokenId) public override whenNotPaused {
|
||||
super.burn(tokenId);
|
||||
_clearHandleHash(tokenId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Burns a profile with a signature, this maintains the profile data struct, but deletes the
|
||||
* handle hash to profile ID mapping value.
|
||||
*
|
||||
* NOTE: This overrides the LensNFTBase contract's `burnWithSig()` function and calls it to fully burn
|
||||
* the NFT.
|
||||
*/
|
||||
function burnWithSig(uint256 tokenId, DataTypes.EIP712Signature calldata sig)
|
||||
public
|
||||
override
|
||||
whenNotPaused
|
||||
{
|
||||
super.burnWithSig(tokenId, sig);
|
||||
_clearHandleHash(tokenId);
|
||||
}
|
||||
|
||||
/// ***************************************
|
||||
/// *****PROFILE INTERACTION FUNCTIONS*****
|
||||
/// ***************************************
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function follow(uint256[] calldata profileIds, bytes[] calldata datas)
|
||||
external
|
||||
override
|
||||
whenNotPaused
|
||||
{
|
||||
InteractionLogic.follow(
|
||||
msg.sender,
|
||||
profileIds,
|
||||
datas,
|
||||
FOLLOW_NFT_IMPL,
|
||||
_profileById,
|
||||
_profileIdByHandleHash
|
||||
);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function followWithSig(DataTypes.FollowWithSigData calldata vars)
|
||||
external
|
||||
override
|
||||
whenNotPaused
|
||||
{
|
||||
bytes32[] memory dataHashes = new bytes32[](vars.datas.length);
|
||||
for (uint256 i = 0; i < vars.datas.length; i++) {
|
||||
dataHashes[i] = keccak256(vars.datas[i]);
|
||||
}
|
||||
|
||||
bytes32 digest;
|
||||
unchecked {
|
||||
digest = keccak256(
|
||||
abi.encodePacked(
|
||||
'\x19\x01',
|
||||
_calculateDomainSeparator(),
|
||||
keccak256(
|
||||
abi.encode(
|
||||
FOLLOW_WITH_SIG_TYPEHASH,
|
||||
keccak256(abi.encodePacked(vars.profileIds)),
|
||||
keccak256(abi.encodePacked(dataHashes)),
|
||||
sigNonces[vars.follower]++,
|
||||
vars.sig.deadline
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
_validateRecoveredAddress(digest, vars.follower, vars.sig);
|
||||
InteractionLogic.follow(
|
||||
vars.follower,
|
||||
vars.profileIds,
|
||||
vars.datas,
|
||||
FOLLOW_NFT_IMPL,
|
||||
_profileById,
|
||||
_profileIdByHandleHash
|
||||
);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function collect(
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) external override whenNotPaused {
|
||||
InteractionLogic.collect(
|
||||
msg.sender,
|
||||
profileId,
|
||||
pubId,
|
||||
data,
|
||||
COLLECT_NFT_IMPL,
|
||||
_pubByIdByProfile,
|
||||
_profileById
|
||||
);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function collectWithSig(DataTypes.CollectWithSigData calldata vars)
|
||||
external
|
||||
override
|
||||
whenNotPaused
|
||||
{
|
||||
bytes32 digest;
|
||||
unchecked {
|
||||
digest = keccak256(
|
||||
abi.encodePacked(
|
||||
'\x19\x01',
|
||||
_calculateDomainSeparator(),
|
||||
keccak256(
|
||||
abi.encode(
|
||||
COLLECT_WITH_SIG_TYPEHASH,
|
||||
vars.profileId,
|
||||
vars.pubId,
|
||||
keccak256(vars.data),
|
||||
sigNonces[vars.collector]++,
|
||||
vars.sig.deadline
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
_validateRecoveredAddress(digest, vars.collector, vars.sig);
|
||||
InteractionLogic.collect(
|
||||
vars.collector,
|
||||
vars.profileId,
|
||||
vars.pubId,
|
||||
vars.data,
|
||||
COLLECT_NFT_IMPL,
|
||||
_pubByIdByProfile,
|
||||
_profileById
|
||||
);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function emitFollowNFTTransferEvent(
|
||||
uint256 profileId,
|
||||
uint256 followNFTId,
|
||||
address from,
|
||||
address to
|
||||
) external override {
|
||||
address expectedFollowNFT = _profileById[profileId].followNFT;
|
||||
if (msg.sender != expectedFollowNFT) revert Errors.CallerNotFollowNFT();
|
||||
emit Events.FollowNFTTransferred(profileId, followNFTId, from, to, block.timestamp);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function emitCollectNFTTransferEvent(
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
uint256 collectNFTId,
|
||||
address from,
|
||||
address to
|
||||
) external override {
|
||||
address expectedCollectNFT = _pubByIdByProfile[profileId][pubId].collectNFT;
|
||||
if (msg.sender != expectedCollectNFT) revert Errors.CallerNotCollectNFT();
|
||||
emit Events.CollectNFTTransferred(
|
||||
profileId,
|
||||
pubId,
|
||||
collectNFTId,
|
||||
from,
|
||||
to,
|
||||
block.timestamp
|
||||
);
|
||||
}
|
||||
|
||||
/// *********************************
|
||||
/// *****EXTERNAL VIEW FUNCTIONS*****
|
||||
/// *********************************
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function isProfileCreatorWhitelisted(address profileCreator)
|
||||
external
|
||||
view
|
||||
override
|
||||
returns (bool)
|
||||
{
|
||||
return _profileCreatorWhitelisted[profileCreator];
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function isFollowModuleWhitelisted(address followModule) external view override returns (bool) {
|
||||
return _followModuleWhitelisted[followModule];
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function isReferenceModuleWhitelisted(address referenceModule)
|
||||
external
|
||||
view
|
||||
override
|
||||
returns (bool)
|
||||
{
|
||||
return _referenceModuleWhitelisted[referenceModule];
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function isCollectModuleWhitelisted(address collectModule)
|
||||
external
|
||||
view
|
||||
override
|
||||
returns (bool)
|
||||
{
|
||||
return _collectModuleWhitelisted[collectModule];
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function getGovernance() external view override returns (address) {
|
||||
return _governance;
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function getDispatcher(uint256 profileId) external view override returns (address) {
|
||||
return _dispatcherByProfile[profileId];
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function getPubCount(uint256 profileId) external view override returns (uint256) {
|
||||
return _profileById[profileId].pubCount;
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function getFollowNFT(uint256 profileId) external view override returns (address) {
|
||||
return _profileById[profileId].followNFT;
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function getFollowNFTURI(uint256 profileId) external view override returns (string memory) {
|
||||
return _profileById[profileId].followNFTURI;
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function getCollectNFT(uint256 profileId, uint256 pubId)
|
||||
external
|
||||
view
|
||||
override
|
||||
returns (address)
|
||||
{
|
||||
return _pubByIdByProfile[profileId][pubId].collectNFT;
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function getFollowModule(uint256 profileId) external view override returns (address) {
|
||||
return _profileById[profileId].followModule;
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function getCollectModule(uint256 profileId, uint256 pubId)
|
||||
external
|
||||
view
|
||||
override
|
||||
returns (address)
|
||||
{
|
||||
return _pubByIdByProfile[profileId][pubId].collectModule;
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function getReferenceModule(uint256 profileId, uint256 pubId)
|
||||
external
|
||||
view
|
||||
override
|
||||
returns (address)
|
||||
{
|
||||
return _pubByIdByProfile[profileId][pubId].referenceModule;
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function getHandle(uint256 profileId) external view override returns (string memory) {
|
||||
return _profileById[profileId].handle;
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function getPubPointer(uint256 profileId, uint256 pubId)
|
||||
external
|
||||
view
|
||||
override
|
||||
returns (uint256, uint256)
|
||||
{
|
||||
uint256 profileIdPointed = _pubByIdByProfile[profileId][pubId].profileIdPointed;
|
||||
uint256 pubIdPointed = _pubByIdByProfile[profileId][pubId].pubIdPointed;
|
||||
return (profileIdPointed, pubIdPointed);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function getContentURI(uint256 profileId, uint256 pubId)
|
||||
external
|
||||
view
|
||||
override
|
||||
returns (string memory)
|
||||
{
|
||||
(uint256 rootProfileId, uint256 rootPubId, ) = Helpers.getPointedIfMirror(
|
||||
profileId,
|
||||
pubId,
|
||||
_pubByIdByProfile
|
||||
);
|
||||
return _pubByIdByProfile[rootProfileId][rootPubId].contentURI;
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function getProfileIdByHandle(string calldata handle) external view override returns (uint256) {
|
||||
bytes32 handleHash = keccak256(bytes(handle));
|
||||
return _profileIdByHandleHash[handleHash];
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function getProfile(uint256 profileId)
|
||||
external
|
||||
view
|
||||
override
|
||||
returns (DataTypes.ProfileStruct memory)
|
||||
{
|
||||
return _profileById[profileId];
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function getPub(uint256 profileId, uint256 pubId)
|
||||
external
|
||||
view
|
||||
override
|
||||
returns (DataTypes.PublicationStruct memory)
|
||||
{
|
||||
return _pubByIdByProfile[profileId][pubId];
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensHub
|
||||
function getPubType(uint256 profileId, uint256 pubId)
|
||||
external
|
||||
view
|
||||
override
|
||||
returns (DataTypes.PubType)
|
||||
{
|
||||
if (pubId == 0 || _profileById[profileId].pubCount < pubId) {
|
||||
return DataTypes.PubType.Nonexistent;
|
||||
} else if (_pubByIdByProfile[profileId][pubId].collectModule == address(0)) {
|
||||
return DataTypes.PubType.Mirror;
|
||||
} else {
|
||||
if (_pubByIdByProfile[profileId][pubId].profileIdPointed == 0) {
|
||||
return DataTypes.PubType.Post;
|
||||
} else {
|
||||
return DataTypes.PubType.Comment;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Overrides the ERC721 tokenURI function to return the associated URI with a given profile.
|
||||
*/
|
||||
function tokenURI(uint256 tokenId) public view override returns (string memory) {
|
||||
return _profileById[tokenId].imageURI; // temp
|
||||
}
|
||||
|
||||
/// ****************************
|
||||
/// *****INTERNAL FUNCTIONS*****
|
||||
/// ****************************
|
||||
|
||||
function _setGovernance(address newGovernance) internal {
|
||||
address prevGovernance = _governance;
|
||||
_governance = newGovernance;
|
||||
emit Events.GovernanceSet(msg.sender, prevGovernance, newGovernance, block.timestamp);
|
||||
}
|
||||
|
||||
function _createPost(
|
||||
uint256 profileId,
|
||||
string memory contentURI,
|
||||
address collectModule,
|
||||
bytes memory collectModuleData,
|
||||
address referenceModule,
|
||||
bytes memory referenceModuleData
|
||||
) internal {
|
||||
PublishingLogic.createPost(
|
||||
profileId,
|
||||
contentURI,
|
||||
collectModule,
|
||||
collectModuleData,
|
||||
referenceModule,
|
||||
referenceModuleData,
|
||||
++_profileById[profileId].pubCount,
|
||||
_pubByIdByProfile,
|
||||
_collectModuleWhitelisted,
|
||||
_referenceModuleWhitelisted
|
||||
);
|
||||
}
|
||||
|
||||
function _createComment(DataTypes.CommentData memory vars) internal {
|
||||
PublishingLogic.createComment(
|
||||
vars,
|
||||
_profileById[vars.profileId].pubCount + 1,
|
||||
_profileById,
|
||||
_pubByIdByProfile,
|
||||
_collectModuleWhitelisted,
|
||||
_referenceModuleWhitelisted
|
||||
);
|
||||
_profileById[vars.profileId].pubCount++;
|
||||
}
|
||||
|
||||
function _createMirror(
|
||||
uint256 profileId,
|
||||
uint256 profileIdPointed,
|
||||
uint256 pubIdPointed,
|
||||
address referenceModule,
|
||||
bytes calldata referenceModuleData
|
||||
) internal {
|
||||
PublishingLogic.createMirror(
|
||||
profileId,
|
||||
profileIdPointed,
|
||||
pubIdPointed,
|
||||
referenceModule,
|
||||
referenceModuleData,
|
||||
++_profileById[profileId].pubCount,
|
||||
_pubByIdByProfile,
|
||||
_referenceModuleWhitelisted
|
||||
);
|
||||
}
|
||||
|
||||
function _setDispatcher(uint256 profileId, address dispatcher) internal {
|
||||
_dispatcherByProfile[profileId] = dispatcher;
|
||||
emit Events.DispatcherSet(profileId, dispatcher, block.timestamp);
|
||||
}
|
||||
|
||||
function _setProfileImageURI(uint256 profileId, string memory imageURI) internal {
|
||||
_profileById[profileId].imageURI = imageURI;
|
||||
emit Events.ProfileImageURISet(profileId, imageURI, block.timestamp);
|
||||
}
|
||||
|
||||
function _setFollowNFTURI(uint256 profileId, string memory followNFTURI) internal {
|
||||
_profileById[profileId].followNFTURI = followNFTURI;
|
||||
emit Events.FollowNFTURISet(profileId, followNFTURI, block.timestamp);
|
||||
}
|
||||
|
||||
function _clearHandleHash(uint256 profileId) internal {
|
||||
bytes32 handleHash = keccak256(bytes(_profileById[profileId].handle));
|
||||
_profileIdByHandleHash[handleHash] = 0;
|
||||
}
|
||||
|
||||
function _beforeTokenTransfer(
|
||||
address from,
|
||||
address to,
|
||||
uint256 tokenId
|
||||
) internal override {
|
||||
if (_dispatcherByProfile[tokenId] != address(0)) {
|
||||
_setDispatcher(tokenId, address(0));
|
||||
}
|
||||
super._beforeTokenTransfer(from, to, tokenId);
|
||||
}
|
||||
|
||||
function _validateCallerIsProfileOwnerOrDispatcher(uint256 profileId) internal view {
|
||||
if (msg.sender != ownerOf(profileId) && msg.sender != _dispatcherByProfile[profileId])
|
||||
revert Errors.NotProfileOwnerOrDispatcher();
|
||||
}
|
||||
|
||||
function _validateCallerIsProfileOwner(uint256 profileId) internal view {
|
||||
if (msg.sender != ownerOf(profileId)) revert Errors.NotProfileOwner();
|
||||
}
|
||||
|
||||
function _validateCallerIsGovernance() internal view {
|
||||
if (msg.sender != _governance) revert Errors.NotGovernance();
|
||||
}
|
||||
|
||||
function _validateCallerIsWhitelistedProfileCreator() internal view {
|
||||
if (!_profileCreatorWhitelisted[msg.sender]) revert Errors.ProfileCreatorNotWhitelisted();
|
||||
}
|
||||
|
||||
function getRevision() internal pure virtual override returns (uint256) {
|
||||
return REVISION;
|
||||
}
|
||||
}
|
||||
181
contracts/core/base/ERC721Enumerable.sol
Normal file
181
contracts/core/base/ERC721Enumerable.sol
Normal file
@@ -0,0 +1,181 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import './ERC721Time.sol';
|
||||
import '@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol';
|
||||
|
||||
/**
|
||||
* @dev This implements an optional extension of {ERC721} defined in the EIP that adds
|
||||
* enumerability of all the token ids in the contract as well as all token ids owned by each
|
||||
* account.
|
||||
*
|
||||
* NOTE: Modified from Openzeppelin to inherit from a modified ERC721 contract.
|
||||
*/
|
||||
abstract contract ERC721Enumerable is ERC721Time, IERC721Enumerable {
|
||||
// Mapping from owner to list of owned token IDs
|
||||
mapping(address => mapping(uint256 => uint256)) private _ownedTokens;
|
||||
|
||||
// Mapping from token ID to index of the owner tokens list
|
||||
mapping(uint256 => uint256) private _ownedTokensIndex;
|
||||
|
||||
// Array with all token ids, used for enumeration
|
||||
uint256[] private _allTokens;
|
||||
|
||||
// Mapping from token id to position in the allTokens array
|
||||
mapping(uint256 => uint256) private _allTokensIndex;
|
||||
|
||||
/**
|
||||
* @dev See {IERC165-supportsInterface}.
|
||||
*/
|
||||
function supportsInterface(bytes4 interfaceId)
|
||||
public
|
||||
view
|
||||
virtual
|
||||
override(IERC165, ERC721Time)
|
||||
returns (bool)
|
||||
{
|
||||
return
|
||||
interfaceId == type(IERC721Enumerable).interfaceId ||
|
||||
super.supportsInterface(interfaceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721Enumerable-tokenOfOwnerByIndex}.
|
||||
*/
|
||||
function tokenOfOwnerByIndex(address owner, uint256 index)
|
||||
public
|
||||
view
|
||||
virtual
|
||||
override
|
||||
returns (uint256)
|
||||
{
|
||||
require(index < ERC721Time.balanceOf(owner), 'ERC721Enumerable: owner index out of bounds');
|
||||
return _ownedTokens[owner][index];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721Enumerable-totalSupply}.
|
||||
*/
|
||||
function totalSupply() public view virtual override returns (uint256) {
|
||||
return _allTokens.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721Enumerable-tokenByIndex}.
|
||||
*/
|
||||
function tokenByIndex(uint256 index) public view virtual override returns (uint256) {
|
||||
require(
|
||||
index < ERC721Enumerable.totalSupply(),
|
||||
'ERC721Enumerable: global index out of bounds'
|
||||
);
|
||||
return _allTokens[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Hook that is called before any token transfer. This includes minting
|
||||
* and burning.
|
||||
*
|
||||
* Calling conditions:
|
||||
*
|
||||
* - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be
|
||||
* transferred to `to`.
|
||||
* - When `from` is zero, `tokenId` will be minted for `to`.
|
||||
* - When `to` is zero, ``from``'s `tokenId` will be burned.
|
||||
* - `from` cannot be the zero address.
|
||||
* - `to` cannot be the zero address.
|
||||
*
|
||||
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
|
||||
*/
|
||||
function _beforeTokenTransfer(
|
||||
address from,
|
||||
address to,
|
||||
uint256 tokenId
|
||||
) internal virtual override {
|
||||
super._beforeTokenTransfer(from, to, tokenId);
|
||||
|
||||
if (from == address(0)) {
|
||||
_addTokenToAllTokensEnumeration(tokenId);
|
||||
} else if (from != to) {
|
||||
_removeTokenFromOwnerEnumeration(from, tokenId);
|
||||
}
|
||||
if (to == address(0)) {
|
||||
_removeTokenFromAllTokensEnumeration(tokenId);
|
||||
} else if (to != from) {
|
||||
_addTokenToOwnerEnumeration(to, tokenId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Private function to add a token to this extension's ownership-tracking data structures.
|
||||
* @param to address representing the new owner of the given token ID
|
||||
* @param tokenId uint256 ID of the token to be added to the tokens list of the given address
|
||||
*/
|
||||
function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private {
|
||||
uint256 length = ERC721Time.balanceOf(to);
|
||||
_ownedTokens[to][length] = tokenId;
|
||||
_ownedTokensIndex[tokenId] = length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Private function to add a token to this extension's token tracking data structures.
|
||||
* @param tokenId uint256 ID of the token to be added to the tokens list
|
||||
*/
|
||||
function _addTokenToAllTokensEnumeration(uint256 tokenId) private {
|
||||
_allTokensIndex[tokenId] = _allTokens.length;
|
||||
_allTokens.push(tokenId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Private function to remove a token from this extension's ownership-tracking data structures. Note that
|
||||
* while the token is not assigned a new owner, the `_ownedTokensIndex` mapping is _not_ updated: this allows for
|
||||
* gas optimizations e.g. when performing a transfer operation (avoiding double writes).
|
||||
* This has O(1) time complexity, but alters the order of the _ownedTokens array.
|
||||
* @param from address representing the previous owner of the given token ID
|
||||
* @param tokenId uint256 ID of the token to be removed from the tokens list of the given address
|
||||
*/
|
||||
function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId) private {
|
||||
// To prevent a gap in from's tokens array, we store the last token in the index of the token to delete, and
|
||||
// then delete the last slot (swap and pop).
|
||||
|
||||
uint256 lastTokenIndex = ERC721Time.balanceOf(from) - 1;
|
||||
uint256 tokenIndex = _ownedTokensIndex[tokenId];
|
||||
|
||||
// When the token to delete is the last token, the swap operation is unnecessary
|
||||
if (tokenIndex != lastTokenIndex) {
|
||||
uint256 lastTokenId = _ownedTokens[from][lastTokenIndex];
|
||||
|
||||
_ownedTokens[from][tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
|
||||
_ownedTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index
|
||||
}
|
||||
|
||||
// This also deletes the contents at the last position of the array
|
||||
delete _ownedTokensIndex[tokenId];
|
||||
delete _ownedTokens[from][lastTokenIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Private function to remove a token from this extension's token tracking data structures.
|
||||
* This has O(1) time complexity, but alters the order of the _allTokens array.
|
||||
* @param tokenId uint256 ID of the token to be removed from the tokens list
|
||||
*/
|
||||
function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private {
|
||||
// To prevent a gap in the tokens array, we store the last token in the index of the token to delete, and
|
||||
// then delete the last slot (swap and pop).
|
||||
|
||||
uint256 lastTokenIndex = _allTokens.length - 1;
|
||||
uint256 tokenIndex = _allTokensIndex[tokenId];
|
||||
|
||||
// When the token to delete is the last token, the swap operation is unnecessary. However, since this occurs so
|
||||
// rarely (when the last minted token is burnt) that we still do the swap here to avoid the gas cost of adding
|
||||
// an 'if' statement (like in _removeTokenFromOwnerEnumeration)
|
||||
uint256 lastTokenId = _allTokens[lastTokenIndex];
|
||||
|
||||
_allTokens[tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
|
||||
_allTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index
|
||||
|
||||
// This also deletes the contents at the last position of the array
|
||||
delete _allTokensIndex[tokenId];
|
||||
_allTokens.pop();
|
||||
}
|
||||
}
|
||||
489
contracts/core/base/ERC721Time.sol
Normal file
489
contracts/core/base/ERC721Time.sol
Normal file
@@ -0,0 +1,489 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import '@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol';
|
||||
import '@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol';
|
||||
import '@openzeppelin/contracts/utils/Address.sol';
|
||||
import '@openzeppelin/contracts/utils/Context.sol';
|
||||
import '@openzeppelin/contracts/utils/Strings.sol';
|
||||
import '@openzeppelin/contracts/utils/introspection/ERC165.sol';
|
||||
import './IERC721Time.sol';
|
||||
|
||||
/**
|
||||
* @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including
|
||||
* the Metadata extension, but not including the Enumerable extension, which is available separately as
|
||||
* {ERC721Enumerable}.
|
||||
*
|
||||
* Modifications:
|
||||
* 1. Refactored _operatorApprovals setter into an internal function to allow meta-transactions.
|
||||
* 2. Constructor replaced with an initializer.
|
||||
* 3. Mint timestamp is now stored in a TokenData struct alongside the owner address.
|
||||
*/
|
||||
abstract contract ERC721Time is Context, ERC165, IERC721Time, IERC721Metadata {
|
||||
using Address for address;
|
||||
using Strings for uint256;
|
||||
|
||||
// Token name
|
||||
string private _name;
|
||||
|
||||
// Token symbol
|
||||
string private _symbol;
|
||||
|
||||
// Mapping from token ID to token Data (owner address and mint timestamp uint96), this
|
||||
// replaces the original mapping(uint256 => address) private _owners;
|
||||
mapping(uint256 => IERC721Time.TokenData) private _tokenData;
|
||||
|
||||
// Mapping owner address to token count
|
||||
mapping(address => uint256) private _balances;
|
||||
|
||||
// Mapping from token ID to approved address
|
||||
mapping(uint256 => address) private _tokenApprovals;
|
||||
|
||||
// Mapping from owner to operator approvals
|
||||
mapping(address => mapping(address => bool)) private _operatorApprovals;
|
||||
|
||||
/**
|
||||
* @dev Initializes the ERC721 name and symbol.
|
||||
*
|
||||
* @param name The name to set.
|
||||
* @param symbol The symbol to set.
|
||||
*/
|
||||
function __ERC721_Init(string calldata name, string calldata symbol) internal {
|
||||
_name = name;
|
||||
_symbol = symbol;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC165-supportsInterface}.
|
||||
*/
|
||||
function supportsInterface(bytes4 interfaceId)
|
||||
public
|
||||
view
|
||||
virtual
|
||||
override(ERC165, IERC165)
|
||||
returns (bool)
|
||||
{
|
||||
return
|
||||
interfaceId == type(IERC721).interfaceId ||
|
||||
interfaceId == type(IERC721Metadata).interfaceId ||
|
||||
super.supportsInterface(interfaceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721-balanceOf}.
|
||||
*/
|
||||
function balanceOf(address owner) public view virtual override returns (uint256) {
|
||||
require(owner != address(0), 'ERC721: balance query for the zero address');
|
||||
return _balances[owner];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721-ownerOf}.
|
||||
*/
|
||||
function ownerOf(uint256 tokenId) public view virtual override returns (address) {
|
||||
address owner = _tokenData[tokenId].owner;
|
||||
require(owner != address(0), 'ERC721: owner query for nonexistent token');
|
||||
return owner;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721Time-mintTimestampOf}
|
||||
*/
|
||||
function mintTimestampOf(uint256 tokenId) public view virtual override returns (uint256) {
|
||||
uint96 mintTimestamp = _tokenData[tokenId].mintTimestamp;
|
||||
require(mintTimestamp != 0, 'ERC721: mint timestamp query for nonexistent token');
|
||||
return mintTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721Time-mintTimestampOf}
|
||||
*/
|
||||
function tokenDataOf(uint256 tokenId)
|
||||
public
|
||||
view
|
||||
virtual
|
||||
override
|
||||
returns (IERC721Time.TokenData memory)
|
||||
{
|
||||
require(_exists(tokenId), 'ERC721: token data query for nonexistent token');
|
||||
return _tokenData[tokenId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721Metadata-name}.
|
||||
*/
|
||||
function name() public view virtual override returns (string memory) {
|
||||
return _name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721Metadata-symbol}.
|
||||
*/
|
||||
function symbol() public view virtual override returns (string memory) {
|
||||
return _symbol;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721Metadata-tokenURI}.
|
||||
*/
|
||||
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
|
||||
require(_exists(tokenId), 'ERC721Metadata: URI query for nonexistent token');
|
||||
|
||||
string memory baseURI = _baseURI();
|
||||
return
|
||||
bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Base URI for computing {tokenURI}. If set, the resulting URI for each
|
||||
* token will be the concatenation of the `baseURI` and the `tokenId`. Empty
|
||||
* by default, can be overriden in child contracts.
|
||||
*/
|
||||
function _baseURI() internal view virtual returns (string memory) {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721-approve}.
|
||||
*/
|
||||
function approve(address to, uint256 tokenId) public virtual override {
|
||||
address owner = ERC721Time.ownerOf(tokenId);
|
||||
require(to != owner, 'ERC721: approval to current owner');
|
||||
|
||||
require(
|
||||
_msgSender() == owner || isApprovedForAll(owner, _msgSender()),
|
||||
'ERC721: approve caller is not owner nor approved for all'
|
||||
);
|
||||
|
||||
_approve(to, tokenId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721-getApproved}.
|
||||
*/
|
||||
function getApproved(uint256 tokenId) public view virtual override returns (address) {
|
||||
require(_exists(tokenId), 'ERC721: approved query for nonexistent token');
|
||||
|
||||
return _tokenApprovals[tokenId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721-setApprovalForAll}.
|
||||
*/
|
||||
function setApprovalForAll(address operator, bool approved) public virtual override {
|
||||
require(operator != _msgSender(), 'ERC721: approve to caller');
|
||||
|
||||
_setOperatorApproval(_msgSender(), operator, approved);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721-isApprovedForAll}.
|
||||
*/
|
||||
function isApprovedForAll(address owner, address operator)
|
||||
public
|
||||
view
|
||||
virtual
|
||||
override
|
||||
returns (bool)
|
||||
{
|
||||
return _operatorApprovals[owner][operator];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721-transferFrom}.
|
||||
*/
|
||||
function transferFrom(
|
||||
address from,
|
||||
address to,
|
||||
uint256 tokenId
|
||||
) public virtual override {
|
||||
//solhint-disable-next-line max-line-length
|
||||
require(
|
||||
_isApprovedOrOwner(_msgSender(), tokenId),
|
||||
'ERC721: transfer caller is not owner nor approved'
|
||||
);
|
||||
|
||||
_transfer(from, to, tokenId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721-safeTransferFrom}.
|
||||
*/
|
||||
function safeTransferFrom(
|
||||
address from,
|
||||
address to,
|
||||
uint256 tokenId
|
||||
) public virtual override {
|
||||
safeTransferFrom(from, to, tokenId, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721-safeTransferFrom}.
|
||||
*/
|
||||
function safeTransferFrom(
|
||||
address from,
|
||||
address to,
|
||||
uint256 tokenId,
|
||||
bytes memory _data
|
||||
) public virtual override {
|
||||
require(
|
||||
_isApprovedOrOwner(_msgSender(), tokenId),
|
||||
'ERC721: transfer caller is not owner nor approved'
|
||||
);
|
||||
_safeTransfer(from, to, tokenId, _data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
|
||||
* are aware of the ERC721 protocol to prevent tokens from being forever locked.
|
||||
*
|
||||
* `_data` is additional data, it has no specified format and it is sent in call to `to`.
|
||||
*
|
||||
* This internal function is equivalent to {safeTransferFrom}, and can be used to e.g.
|
||||
* implement alternative mechanisms to perform token transfer, such as signature-based.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `from` cannot be the zero address.
|
||||
* - `to` cannot be the zero address.
|
||||
* - `tokenId` token must exist and be owned by `from`.
|
||||
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
|
||||
*
|
||||
* Emits a {Transfer} event.
|
||||
*/
|
||||
function _safeTransfer(
|
||||
address from,
|
||||
address to,
|
||||
uint256 tokenId,
|
||||
bytes memory _data
|
||||
) internal virtual {
|
||||
_transfer(from, to, tokenId);
|
||||
require(
|
||||
_checkOnERC721Received(from, to, tokenId, _data),
|
||||
'ERC721: transfer to non ERC721Receiver implementer'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns whether `tokenId` exists.
|
||||
*
|
||||
* Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}.
|
||||
*
|
||||
* Tokens start existing when they are minted (`_mint`),
|
||||
* and stop existing when they are burned (`_burn`).
|
||||
*/
|
||||
function _exists(uint256 tokenId) internal view virtual returns (bool) {
|
||||
return _tokenData[tokenId].owner != address(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns whether `spender` is allowed to manage `tokenId`.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `tokenId` must exist.
|
||||
*/
|
||||
function _isApprovedOrOwner(address spender, uint256 tokenId)
|
||||
internal
|
||||
view
|
||||
virtual
|
||||
returns (bool)
|
||||
{
|
||||
require(_exists(tokenId), 'ERC721: operator query for nonexistent token');
|
||||
address owner = ERC721Time.ownerOf(tokenId);
|
||||
return (spender == owner ||
|
||||
getApproved(tokenId) == spender ||
|
||||
isApprovedForAll(owner, spender));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Safely mints `tokenId` and transfers it to `to`.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `tokenId` must not exist.
|
||||
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
|
||||
*
|
||||
* Emits a {Transfer} event.
|
||||
*/
|
||||
function _safeMint(address to, uint256 tokenId) internal virtual {
|
||||
_safeMint(to, tokenId, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is
|
||||
* forwarded in {IERC721Receiver-onERC721Received} to contract recipients.
|
||||
*/
|
||||
function _safeMint(
|
||||
address to,
|
||||
uint256 tokenId,
|
||||
bytes memory _data
|
||||
) internal virtual {
|
||||
_mint(to, tokenId);
|
||||
require(
|
||||
_checkOnERC721Received(address(0), to, tokenId, _data),
|
||||
'ERC721: transfer to non ERC721Receiver implementer'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Mints `tokenId` and transfers it to `to`.
|
||||
*
|
||||
* WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `tokenId` must not exist.
|
||||
* - `to` cannot be the zero address.
|
||||
*
|
||||
* Emits a {Transfer} event.
|
||||
*/
|
||||
function _mint(address to, uint256 tokenId) internal virtual {
|
||||
require(to != address(0), 'ERC721: mint to the zero address');
|
||||
require(!_exists(tokenId), 'ERC721: token already minted');
|
||||
|
||||
_beforeTokenTransfer(address(0), to, tokenId);
|
||||
|
||||
_balances[to] += 1;
|
||||
_tokenData[tokenId].owner = to;
|
||||
_tokenData[tokenId].mintTimestamp = uint96(block.timestamp);
|
||||
|
||||
emit Transfer(address(0), to, tokenId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Destroys `tokenId`.
|
||||
* The approval is cleared when the token is burned.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `tokenId` must exist.
|
||||
*
|
||||
* Emits a {Transfer} event.
|
||||
*/
|
||||
function _burn(uint256 tokenId) internal virtual {
|
||||
address owner = ERC721Time.ownerOf(tokenId);
|
||||
|
||||
_beforeTokenTransfer(owner, address(0), tokenId);
|
||||
|
||||
// Clear approvals
|
||||
_approve(address(0), tokenId);
|
||||
|
||||
_balances[owner] -= 1;
|
||||
delete _tokenData[tokenId];
|
||||
|
||||
emit Transfer(owner, address(0), tokenId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Transfers `tokenId` from `from` to `to`.
|
||||
* As opposed to {transferFrom}, this imposes no restrictions on msg.sender.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `to` cannot be the zero address.
|
||||
* - `tokenId` token must be owned by `from`.
|
||||
*
|
||||
* Emits a {Transfer} event.
|
||||
*/
|
||||
function _transfer(
|
||||
address from,
|
||||
address to,
|
||||
uint256 tokenId
|
||||
) internal virtual {
|
||||
require(ERC721Time.ownerOf(tokenId) == from, 'ERC721: transfer of token that is not own');
|
||||
require(to != address(0), 'ERC721: transfer to the zero address');
|
||||
|
||||
_beforeTokenTransfer(from, to, tokenId);
|
||||
|
||||
// Clear approvals from the previous owner
|
||||
_approve(address(0), tokenId);
|
||||
|
||||
_balances[from] -= 1;
|
||||
_balances[to] += 1;
|
||||
_tokenData[tokenId].owner = to;
|
||||
|
||||
emit Transfer(from, to, tokenId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Approve `to` to operate on `tokenId`
|
||||
*
|
||||
* Emits a {Approval} event.
|
||||
*/
|
||||
function _approve(address to, uint256 tokenId) internal virtual {
|
||||
_tokenApprovals[tokenId] = to;
|
||||
emit Approval(ERC721Time.ownerOf(tokenId), to, tokenId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Refactored from the original OZ ERC721 implementation: approve or revoke approval from
|
||||
* `operator` to operate on all tokens owned by `owner`.
|
||||
*
|
||||
* Emits a {ApprovalForAll} event.
|
||||
*/
|
||||
function _setOperatorApproval(
|
||||
address owner,
|
||||
address operator,
|
||||
bool approved
|
||||
) internal virtual {
|
||||
_operatorApprovals[owner][operator] = approved;
|
||||
emit ApprovalForAll(owner, operator, approved);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target address.
|
||||
* The call is not executed if the target address is not a contract.
|
||||
*
|
||||
* @param from address representing the previous owner of the given token ID
|
||||
* @param to target address that will receive the tokens
|
||||
* @param tokenId uint256 ID of the token to be transferred
|
||||
* @param _data bytes optional data to send along with the call
|
||||
* @return bool whether the call correctly returned the expected magic value
|
||||
*/
|
||||
function _checkOnERC721Received(
|
||||
address from,
|
||||
address to,
|
||||
uint256 tokenId,
|
||||
bytes memory _data
|
||||
) private returns (bool) {
|
||||
if (to.isContract()) {
|
||||
try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (
|
||||
bytes4 retval
|
||||
) {
|
||||
return retval == IERC721Receiver.onERC721Received.selector;
|
||||
} catch (bytes memory reason) {
|
||||
if (reason.length == 0) {
|
||||
revert('ERC721: transfer to non ERC721Receiver implementer');
|
||||
} else {
|
||||
assembly {
|
||||
revert(add(32, reason), mload(reason))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Hook that is called before any token transfer. This includes minting
|
||||
* and burning.
|
||||
*
|
||||
* Calling conditions:
|
||||
*
|
||||
* - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be
|
||||
* transferred to `to`.
|
||||
* - When `from` is zero, `tokenId` will be minted for `to`.
|
||||
* - When `to` is zero, ``from``'s `tokenId` will be burned.
|
||||
* - `from` and `to` are never both zero.
|
||||
*
|
||||
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
|
||||
*/
|
||||
function _beforeTokenTransfer(
|
||||
address from,
|
||||
address to,
|
||||
uint256 tokenId
|
||||
) internal virtual {}
|
||||
}
|
||||
48
contracts/core/base/IERC721Time.sol
Normal file
48
contracts/core/base/IERC721Time.sol
Normal file
@@ -0,0 +1,48 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import '@openzeppelin/contracts/token/ERC721/IERC721.sol';
|
||||
|
||||
/**
|
||||
* @title IERC721Time
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This is an expansion of the IERC721 interface that includes a struct for token data,
|
||||
* which contains the token owner and the mint timestamp as well as associated getters.
|
||||
*/
|
||||
interface IERC721Time is IERC721 {
|
||||
/**
|
||||
* @notice Contains the owner address and the mint timestamp for every NFT.
|
||||
*
|
||||
* Note: Instead of the owner address in the _tokenOwners private mapping, we now store it in the
|
||||
* _tokenData mapping, alongside the unchanging mintTimestamp.
|
||||
*
|
||||
* @param owner The token owner.
|
||||
* @param mintTimestamp The mint timestamp.
|
||||
*/
|
||||
struct TokenData {
|
||||
address owner;
|
||||
uint96 mintTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Returns the mint timestamp associated with a given NFT, stored only once upon initial mint.
|
||||
*
|
||||
* @param tokenId The token ID of the NFT to query the mint timestamp for.
|
||||
*
|
||||
* @return The mint timestamp, this is stored as a uint96 but returned as a uint256 to reduce unnecessary
|
||||
* padding.
|
||||
*/
|
||||
function mintTimestampOf(uint256 tokenId) external view returns (uint256);
|
||||
|
||||
/**
|
||||
* @notice Returns the token data associated with a given NFT. This allows fetching the token owner and
|
||||
* mint timestamp in a single call.
|
||||
*
|
||||
* @param tokenId The token ID of the NFT to query the token data for.
|
||||
*
|
||||
* @return The token data struct containing both the owner address and the mint timestamp.
|
||||
*/
|
||||
function tokenDataOf(uint256 tokenId) external view returns (TokenData memory);
|
||||
}
|
||||
52
contracts/core/base/LensMultiState.sol
Normal file
52
contracts/core/base/LensMultiState.sol
Normal file
@@ -0,0 +1,52 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {Events} from '../../libraries/Events.sol';
|
||||
import {DataTypes} from '../../libraries/DataTypes.sol';
|
||||
import {Errors} from '../../libraries/Errors.sol';
|
||||
|
||||
/**
|
||||
* @title LensMultiState
|
||||
*
|
||||
* @notice This is an abstract contract that implements internal LensHub state setting and
|
||||
* validation.
|
||||
*/
|
||||
abstract contract LensMultiState {
|
||||
DataTypes.ProtocolState private _state;
|
||||
|
||||
modifier whenNotPaused() {
|
||||
_validateNotPaused();
|
||||
_;
|
||||
}
|
||||
|
||||
modifier whenPublishingEnabled() {
|
||||
_validatePublishingEnabled();
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns the current protocol state.
|
||||
*/
|
||||
function getState() external view returns (DataTypes.ProtocolState) {
|
||||
return _state;
|
||||
}
|
||||
|
||||
function _setState(DataTypes.ProtocolState newState) internal {
|
||||
DataTypes.ProtocolState prevState = _state;
|
||||
_state = newState;
|
||||
emit Events.StateSet(msg.sender, prevState, newState, block.timestamp);
|
||||
}
|
||||
|
||||
function _validatePublishingEnabled() internal view {
|
||||
if (_state == DataTypes.ProtocolState.Paused) {
|
||||
revert Errors.Paused();
|
||||
} else if (_state == DataTypes.ProtocolState.PublishingPaused) {
|
||||
revert Errors.PublishingPaused();
|
||||
}
|
||||
}
|
||||
|
||||
function _validateNotPaused() internal view {
|
||||
if (_state == DataTypes.ProtocolState.Paused) revert Errors.Paused();
|
||||
}
|
||||
}
|
||||
185
contracts/core/base/LensNFTBase.sol
Normal file
185
contracts/core/base/LensNFTBase.sol
Normal file
@@ -0,0 +1,185 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {ILensNFTBase} from '../../interfaces/ILensNFTBase.sol';
|
||||
import {Errors} from '../../libraries/Errors.sol';
|
||||
import {DataTypes} from '../../libraries/DataTypes.sol';
|
||||
import {Events} from '../../libraries/Events.sol';
|
||||
import {ERC721Time} from './ERC721Time.sol';
|
||||
import {ERC721Enumerable} from './ERC721Enumerable.sol';
|
||||
|
||||
abstract contract LensNFTBase is ILensNFTBase, ERC721Enumerable {
|
||||
bytes32 internal constant EIP712_REVISION_HASH =
|
||||
0xc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6;
|
||||
// keccak256('1');
|
||||
bytes32 internal constant PERMIT_TYPEHASH =
|
||||
0x49ecf333e5b8c95c40fdafc95c1ad136e8914a8fb55e9dc8bb01eaa83a2df9ad;
|
||||
// keccak256('Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)');
|
||||
bytes32 internal constant PERMIT_FOR_ALL_TYPEHASH =
|
||||
0x47ab88482c90e4bb94b82a947ae78fa91fb25de1469ab491f4c15b9a0a2677ee;
|
||||
// keccak256(
|
||||
// 'PermitForAll(address owner,address operator,bool approved,uint256 nonce,uint256 deadline)'
|
||||
// );
|
||||
bytes32 internal constant BURN_WITH_SIG_TYPEHASH =
|
||||
0x108ccda6d7331b00561a3eea66a2ae331622356585681c62731e4a01aae2261a;
|
||||
// keccak256('BurnWithSig(uint256 tokenId,uint256 nonce,uint256 deadline)');
|
||||
bytes32 internal constant EIP712_DOMAIN_TYPEHASH =
|
||||
0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f;
|
||||
// keccak256(
|
||||
// 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'
|
||||
// )
|
||||
|
||||
mapping(address => uint256) public sigNonces;
|
||||
|
||||
/**
|
||||
* @notice Initializer sets the name, symbol and the cached domain separator.
|
||||
*
|
||||
* NOTE: Inheritor contracts *must* call this function to initialize the name & symbol in the
|
||||
* inherited ERC721 contract.
|
||||
*
|
||||
* @param name The name to set in the ERC721 contract.
|
||||
* @param symbol The symbol to set in the ERC721 contract.
|
||||
*/
|
||||
function _initialize(string calldata name, string calldata symbol) internal {
|
||||
ERC721Time.__ERC721_Init(name, symbol);
|
||||
|
||||
emit Events.BaseInitialized(name, symbol, block.timestamp);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensNFTBase
|
||||
function permit(
|
||||
address spender,
|
||||
uint256 tokenId,
|
||||
DataTypes.EIP712Signature calldata sig
|
||||
) external override {
|
||||
if (spender == address(0)) revert Errors.ZeroSpender();
|
||||
address owner = ownerOf(tokenId);
|
||||
|
||||
bytes32 digest;
|
||||
unchecked {
|
||||
digest = keccak256(
|
||||
abi.encodePacked(
|
||||
'\x19\x01',
|
||||
_calculateDomainSeparator(),
|
||||
keccak256(
|
||||
abi.encode(
|
||||
PERMIT_TYPEHASH,
|
||||
spender,
|
||||
tokenId,
|
||||
sigNonces[owner]++,
|
||||
sig.deadline
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
_validateRecoveredAddress(digest, owner, sig);
|
||||
_approve(spender, tokenId);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensNFTBase
|
||||
function permitForAll(
|
||||
address owner,
|
||||
address operator,
|
||||
bool approved,
|
||||
DataTypes.EIP712Signature calldata sig
|
||||
) external override {
|
||||
if (operator == address(0)) revert Errors.ZeroSpender();
|
||||
|
||||
bytes32 digest;
|
||||
unchecked {
|
||||
digest = keccak256(
|
||||
abi.encodePacked(
|
||||
'\x19\x01',
|
||||
_calculateDomainSeparator(),
|
||||
keccak256(
|
||||
abi.encode(
|
||||
PERMIT_FOR_ALL_TYPEHASH,
|
||||
owner,
|
||||
operator,
|
||||
approved,
|
||||
sigNonces[owner]++,
|
||||
sig.deadline
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
_validateRecoveredAddress(digest, owner, sig);
|
||||
_setOperatorApproval(owner, operator, approved);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensNFTBase
|
||||
function getDomainSeparator() external view override returns (bytes32) {
|
||||
return _calculateDomainSeparator();
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensNFTBase
|
||||
function burn(uint256 tokenId) public virtual override {
|
||||
if (!_isApprovedOrOwner(msg.sender, tokenId)) revert Errors.NotOwnerOrApproved();
|
||||
_burn(tokenId);
|
||||
}
|
||||
|
||||
/// @inheritdoc ILensNFTBase
|
||||
function burnWithSig(uint256 tokenId, DataTypes.EIP712Signature calldata sig)
|
||||
public
|
||||
virtual
|
||||
override
|
||||
{
|
||||
address owner = ownerOf(tokenId);
|
||||
|
||||
bytes32 digest;
|
||||
unchecked {
|
||||
digest = keccak256(
|
||||
abi.encodePacked(
|
||||
'\x19\x01',
|
||||
_calculateDomainSeparator(),
|
||||
keccak256(
|
||||
abi.encode(
|
||||
BURN_WITH_SIG_TYPEHASH,
|
||||
tokenId,
|
||||
sigNonces[owner]++,
|
||||
sig.deadline
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
_validateRecoveredAddress(digest, owner, sig);
|
||||
_burn(tokenId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Wrapper for ecrecover to reduce code size, used in meta-tx specific functions.
|
||||
*/
|
||||
function _validateRecoveredAddress(
|
||||
bytes32 digest,
|
||||
address expectedAddress,
|
||||
DataTypes.EIP712Signature memory sig
|
||||
) internal view {
|
||||
if (sig.deadline < block.timestamp) revert Errors.SignatureExpired();
|
||||
address recoveredAddress = ecrecover(digest, sig.v, sig.r, sig.s);
|
||||
if (recoveredAddress == address(0) || recoveredAddress != expectedAddress)
|
||||
revert Errors.SignatureInvalid();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Calculates EIP712 DOMAIN_SEPARATOR based on the current contract and chain ID.
|
||||
*/
|
||||
function _calculateDomainSeparator() internal view returns (bytes32) {
|
||||
return
|
||||
keccak256(
|
||||
abi.encode(
|
||||
EIP712_DOMAIN_TYPEHASH,
|
||||
keccak256(bytes(name())),
|
||||
EIP712_REVISION_HASH,
|
||||
block.chainid,
|
||||
address(this)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
44
contracts/core/modules/FeeModuleBase.sol
Normal file
44
contracts/core/modules/FeeModuleBase.sol
Normal file
@@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {Errors} from '../../libraries/Errors.sol';
|
||||
import {Events} from '../../libraries/Events.sol';
|
||||
import {IModuleGlobals} from '../../interfaces/IModuleGlobals.sol';
|
||||
|
||||
/**
|
||||
* @title FeeModuleBase
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This is an abstract contract to be inherited from by modules that require basic fee functionality. It
|
||||
* contains getters for module globals parameters as well as a validation function to check expected data.
|
||||
*/
|
||||
abstract contract FeeModuleBase {
|
||||
uint16 internal constant BPS_MAX = 10000;
|
||||
|
||||
address public immutable MODULE_GLOBALS;
|
||||
|
||||
constructor(address moduleGlobals) {
|
||||
if (moduleGlobals == address(0)) revert Errors.InitParamsInvalid();
|
||||
MODULE_GLOBALS = moduleGlobals;
|
||||
emit Events.FeeModuleBaseConstructed(moduleGlobals, block.timestamp);
|
||||
}
|
||||
|
||||
function _currencyWhitelisted(address currency) internal view returns (bool) {
|
||||
return IModuleGlobals(MODULE_GLOBALS).isCurrencyWhitelisted(currency);
|
||||
}
|
||||
|
||||
function _treasuryData() internal view returns (address, uint16) {
|
||||
return IModuleGlobals(MODULE_GLOBALS).getTreasuryData();
|
||||
}
|
||||
|
||||
function _validateDataIsExpected(
|
||||
bytes calldata data,
|
||||
address currency,
|
||||
uint256 amount
|
||||
) internal pure {
|
||||
(address decodedCurrency, uint256 decodedAmount) = abi.decode(data, (address, uint256));
|
||||
if (decodedAmount != amount || decodedCurrency != currency)
|
||||
revert Errors.ModuleDataMismatch();
|
||||
}
|
||||
}
|
||||
34
contracts/core/modules/FollowValidationModuleBase.sol
Normal file
34
contracts/core/modules/FollowValidationModuleBase.sol
Normal file
@@ -0,0 +1,34 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {IFollowModule} from '../../interfaces/IFollowModule.sol';
|
||||
import {ILensHub} from '../../interfaces/ILensHub.sol';
|
||||
import {Errors} from '../../libraries/Errors.sol';
|
||||
import {Events} from '../../libraries/Events.sol';
|
||||
import {ModuleBase} from './ModuleBase.sol';
|
||||
import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
|
||||
|
||||
/**
|
||||
* @title FollowValidationModuleBase
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This abstract contract adds a simple non-specific follow validation function.
|
||||
*
|
||||
* NOTE: Both the `HUB` variable and `_checkFollowValidity()` function are exposed to inheriting
|
||||
* contracts.
|
||||
*
|
||||
* NOTE: This is only compatible with COLLECT & REFERENCE MODULES.
|
||||
*/
|
||||
abstract contract FollowValidationModuleBase is ModuleBase {
|
||||
function _checkFollowValidity(uint256 profileId, address user) internal view {
|
||||
address followModule = ILensHub(HUB).getFollowModule(profileId);
|
||||
if (followModule != address(0)) {
|
||||
IFollowModule(followModule).validateFollow(profileId, user, 0);
|
||||
} else {
|
||||
address followNFT = ILensHub(HUB).getFollowNFT(profileId);
|
||||
if (followNFT == address(0)) revert Errors.FollowInvalid();
|
||||
if (IERC721(followNFT).balanceOf(user) == 0) revert Errors.FollowInvalid();
|
||||
}
|
||||
}
|
||||
}
|
||||
28
contracts/core/modules/ModuleBase.sol
Normal file
28
contracts/core/modules/ModuleBase.sol
Normal file
@@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {Errors} from '../../libraries/Errors.sol';
|
||||
import {Events} from '../../libraries/Events.sol';
|
||||
|
||||
/**
|
||||
* @title ModuleBase
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This abstract contract adds a public `HUB` immutable to inheriting modules, as well as an
|
||||
* `onlyHub` modifier.
|
||||
*/
|
||||
abstract contract ModuleBase {
|
||||
address public immutable HUB;
|
||||
|
||||
modifier onlyHub() {
|
||||
if (msg.sender != HUB) revert Errors.NotHub();
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address hub) {
|
||||
if (hub == address(0)) revert Errors.InitParamsInvalid();
|
||||
HUB = hub;
|
||||
emit Events.ModuleBaseConstructed(hub, block.timestamp);
|
||||
}
|
||||
}
|
||||
126
contracts/core/modules/ModuleGlobals.sol
Normal file
126
contracts/core/modules/ModuleGlobals.sol
Normal file
@@ -0,0 +1,126 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {Errors} from '../../libraries/Errors.sol';
|
||||
import {Events} from '../../libraries/Events.sol';
|
||||
import {IModuleGlobals} from '../../interfaces/IModuleGlobals.sol';
|
||||
|
||||
/**
|
||||
* @title ModuleGlobals
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This contract contains data relevant to Lens modules, such as the module governance address, treasury
|
||||
* address and treasury fee BPS.
|
||||
*
|
||||
* NOTE: The reason we have an additional governance address instead of just fetching it from the hub is to
|
||||
* allow the flexibility of using different governance executors.
|
||||
*/
|
||||
contract ModuleGlobals is IModuleGlobals {
|
||||
uint16 internal constant BPS_MAX = 10000;
|
||||
|
||||
mapping(address => bool) internal _currencyWhitelisted;
|
||||
address internal _governance;
|
||||
address internal _treasury;
|
||||
uint16 internal _treasuryFee;
|
||||
|
||||
modifier onlyGov() {
|
||||
if (msg.sender != _governance) revert Errors.NotGovernance();
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Initializes the governance, treasury and treasury fee amounts.
|
||||
*
|
||||
* @param governance The governance address which has additional control over setting certain parameters.
|
||||
* @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
|
||||
) {
|
||||
_setGovernance(governance);
|
||||
_setTreasury(treasury);
|
||||
_setTreasuryFee(treasuryFee);
|
||||
}
|
||||
|
||||
/// @inheritdoc IModuleGlobals
|
||||
function setGovernance(address newGovernance) external override onlyGov {
|
||||
_setGovernance(newGovernance);
|
||||
}
|
||||
|
||||
/// @inheritdoc IModuleGlobals
|
||||
function setTreasury(address newTreasury) external override onlyGov {
|
||||
_setTreasury(newTreasury);
|
||||
}
|
||||
|
||||
/// @inheritdoc IModuleGlobals
|
||||
function setTreasuryFee(uint16 newTreasuryFee) external override onlyGov {
|
||||
_setTreasuryFee(newTreasuryFee);
|
||||
}
|
||||
|
||||
/// @inheritdoc IModuleGlobals
|
||||
function whitelistCurrency(address currency, bool toWhitelist) external override onlyGov {
|
||||
_whitelistCurrency(currency, toWhitelist);
|
||||
}
|
||||
|
||||
/// @inheritdoc IModuleGlobals
|
||||
function isCurrencyWhitelisted(address currency) external view override returns (bool) {
|
||||
return _currencyWhitelisted[currency];
|
||||
}
|
||||
|
||||
/// @inheritdoc IModuleGlobals
|
||||
function getGovernance() external view override returns (address) {
|
||||
return _governance;
|
||||
}
|
||||
|
||||
/// @inheritdoc IModuleGlobals
|
||||
function getTreasury() external view override returns (address) {
|
||||
return _treasury;
|
||||
}
|
||||
|
||||
/// @inheritdoc IModuleGlobals
|
||||
function getTreasuryFee() external view override returns (uint16) {
|
||||
return _treasuryFee;
|
||||
}
|
||||
|
||||
//@inheritdoc IModuleGlobals
|
||||
function getTreasuryData() external view override returns (address, uint16) {
|
||||
return (_treasury, _treasuryFee);
|
||||
}
|
||||
|
||||
function _setGovernance(address newGovernance) internal {
|
||||
if (newGovernance == address(0)) revert Errors.InitParamsInvalid();
|
||||
address prevGovernance = _governance;
|
||||
_governance = newGovernance;
|
||||
emit Events.ModuleGlobalsGovernanceSet(prevGovernance, newGovernance, block.timestamp);
|
||||
}
|
||||
|
||||
function _setTreasury(address newTreasury) internal {
|
||||
if (newTreasury == address(0)) revert Errors.InitParamsInvalid();
|
||||
address prevTreasury = _treasury;
|
||||
_treasury = newTreasury;
|
||||
emit Events.ModuleGlobalsTreasurySet(prevTreasury, newTreasury, block.timestamp);
|
||||
}
|
||||
|
||||
function _setTreasuryFee(uint16 newTreasuryFee) internal {
|
||||
if (newTreasuryFee >= BPS_MAX / 2) revert Errors.InitParamsInvalid();
|
||||
uint16 prevTreasuryFee = _treasuryFee;
|
||||
_treasuryFee = newTreasuryFee;
|
||||
emit Events.ModuleGlobalsTreasuryFeeSet(prevTreasuryFee, newTreasuryFee, block.timestamp);
|
||||
}
|
||||
|
||||
function _whitelistCurrency(address currency, bool toWhitelist) internal {
|
||||
if (currency == address(0)) revert Errors.InitParamsInvalid();
|
||||
bool prevWhitelisted = _currencyWhitelisted[currency];
|
||||
_currencyWhitelisted[currency] = toWhitelist;
|
||||
emit Events.ModuleGlobalsCurrencyWhitelisted(
|
||||
currency,
|
||||
prevWhitelisted,
|
||||
toWhitelist,
|
||||
block.timestamp
|
||||
);
|
||||
}
|
||||
}
|
||||
44
contracts/core/modules/collect/EmptyCollectModule.sol
Normal file
44
contracts/core/modules/collect/EmptyCollectModule.sol
Normal file
@@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {ICollectModule} from '../../../interfaces/ICollectModule.sol';
|
||||
import {ModuleBase} from '../ModuleBase.sol';
|
||||
import {FollowValidationModuleBase} from '../FollowValidationModuleBase.sol';
|
||||
|
||||
/**
|
||||
* @title EmptyCollectModule
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This is a simple Lens CollectModule implementation, inheriting from the ICollectModule interface.
|
||||
*
|
||||
* This module works by allowing all collects by followers.
|
||||
*/
|
||||
contract EmptyCollectModule is ICollectModule, FollowValidationModuleBase {
|
||||
constructor(address hub) ModuleBase(hub) {}
|
||||
|
||||
/**
|
||||
* @dev There is nothing needed at initialization.
|
||||
*/
|
||||
function initializePublicationCollectModule(
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) external pure override returns (bytes memory) {
|
||||
return new bytes(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Processes a collect by:
|
||||
* 1. Ensuring the collector is a follower
|
||||
*/
|
||||
function processCollect(
|
||||
uint256 referrerProfileId,
|
||||
address collector,
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) external view override {
|
||||
_checkFollowValidity(profileId, collector);
|
||||
}
|
||||
}
|
||||
178
contracts/core/modules/collect/FeeCollectModule.sol
Normal file
178
contracts/core/modules/collect/FeeCollectModule.sol
Normal file
@@ -0,0 +1,178 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {ICollectModule} from '../../../interfaces/ICollectModule.sol';
|
||||
import {Errors} from '../../../libraries/Errors.sol';
|
||||
import {FeeModuleBase} from '../FeeModuleBase.sol';
|
||||
import {ModuleBase} from '../ModuleBase.sol';
|
||||
import {FollowValidationModuleBase} from '../FollowValidationModuleBase.sol';
|
||||
import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
|
||||
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
|
||||
import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
|
||||
|
||||
/**
|
||||
* @notice A struct containing the necessary data to execute collect actions on a publication.
|
||||
*
|
||||
* @param amount The collecting cost associated with this publication.
|
||||
* @param recipient The recipient address associated with this publication.
|
||||
* @param currency The currency associated with this publication.
|
||||
* @param referralFee The referral fee associated with this publication.
|
||||
*/
|
||||
struct ProfilePublicationData {
|
||||
uint256 amount;
|
||||
address recipient;
|
||||
address currency;
|
||||
uint16 referralFee;
|
||||
}
|
||||
|
||||
/**
|
||||
* @title FeeCollectModule
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This is a simple Lens CollectModule implementation, inheriting from the ICollectModule interface and
|
||||
* the FeeCollectModuleBase abstract contract.
|
||||
*
|
||||
* This module works by allowing unlimited collects for a publication at a given price.
|
||||
*/
|
||||
contract FeeCollectModule is ICollectModule, FeeModuleBase, FollowValidationModuleBase {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
mapping(uint256 => mapping(uint256 => ProfilePublicationData))
|
||||
internal _dataByPublicationByProfile;
|
||||
|
||||
constructor(address hub, address moduleGlobals) FeeModuleBase(moduleGlobals) ModuleBase(hub) {}
|
||||
|
||||
/**
|
||||
* @notice This collect module levies a fee on collects and supports referrals. Thus, we need to decode data.
|
||||
*
|
||||
* @param profileId The token ID of the profile of the publisher, passed by the hub.
|
||||
* @param pubId The publication ID of the newly created publication, passed by the hub.
|
||||
* @param data The arbitrary data parameter, decoded into:
|
||||
* uint256 amount: The currency total amount to levy.
|
||||
* address currency: The currency address, must be internally whitelisted.
|
||||
* address recipient: The custom recipient address to direct earnings to.
|
||||
* uint16 referralFee: The referral fee to set.
|
||||
*
|
||||
* @return An abi encoded bytes parameter, which is the same as the passed data parameter.
|
||||
*/
|
||||
function initializePublicationCollectModule(
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) external override onlyHub returns (bytes memory) {
|
||||
(uint256 amount, address currency, address recipient, uint16 referralFee) = abi.decode(
|
||||
data,
|
||||
(uint256, address, address, uint16)
|
||||
);
|
||||
if (
|
||||
!_currencyWhitelisted(currency) ||
|
||||
recipient == address(0) ||
|
||||
referralFee > BPS_MAX ||
|
||||
amount < BPS_MAX
|
||||
) revert Errors.InitParamsInvalid();
|
||||
|
||||
_dataByPublicationByProfile[profileId][pubId].referralFee = referralFee;
|
||||
_dataByPublicationByProfile[profileId][pubId].recipient = recipient;
|
||||
_dataByPublicationByProfile[profileId][pubId].currency = currency;
|
||||
_dataByPublicationByProfile[profileId][pubId].amount = amount;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Processes a collect by:
|
||||
* 1. Ensuring the collector is a follower
|
||||
* 2. Charging a fee
|
||||
*/
|
||||
function processCollect(
|
||||
uint256 referrerProfileId,
|
||||
address collector,
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) external virtual override onlyHub {
|
||||
_checkFollowValidity(profileId, collector);
|
||||
if (referrerProfileId == profileId) {
|
||||
_processCollect(collector, profileId, pubId, data);
|
||||
} else {
|
||||
_processCollectWithReferral(referrerProfileId, collector, profileId, pubId, data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Returns the publication data for a given publication, or an empty struct if that publication was not
|
||||
* initialized with this module.
|
||||
*
|
||||
* @param profileId The token ID of the profile mapped to the publication to query.
|
||||
* @param pubId The publication ID of the publication to query.
|
||||
*
|
||||
* @return The ProfilePublicationData struct mapped to that publication.
|
||||
*/
|
||||
function getPublicationData(uint256 profileId, uint256 pubId)
|
||||
external
|
||||
view
|
||||
returns (ProfilePublicationData memory)
|
||||
{
|
||||
return _dataByPublicationByProfile[profileId][pubId];
|
||||
}
|
||||
|
||||
function _processCollect(
|
||||
address collector,
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) internal {
|
||||
uint256 amount = _dataByPublicationByProfile[profileId][pubId].amount;
|
||||
address currency = _dataByPublicationByProfile[profileId][pubId].currency;
|
||||
_validateDataIsExpected(data, currency, amount);
|
||||
|
||||
(address treasury, uint16 treasuryFee) = _treasuryData();
|
||||
address recipient = _dataByPublicationByProfile[profileId][pubId].recipient;
|
||||
uint256 treasuryAmount = (amount * treasuryFee) / BPS_MAX;
|
||||
uint256 adjustedAmount = amount - treasuryAmount;
|
||||
|
||||
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
|
||||
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
|
||||
}
|
||||
|
||||
function _processCollectWithReferral(
|
||||
uint256 referrerProfileId,
|
||||
address collector,
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) internal {
|
||||
uint256 amount = _dataByPublicationByProfile[profileId][pubId].amount;
|
||||
address currency = _dataByPublicationByProfile[profileId][pubId].currency;
|
||||
_validateDataIsExpected(data, currency, amount);
|
||||
|
||||
uint256 referralFee = _dataByPublicationByProfile[profileId][pubId].referralFee;
|
||||
address treasury;
|
||||
uint256 treasuryAmount;
|
||||
|
||||
// Avoids stack too deep
|
||||
{
|
||||
uint16 treasuryFee;
|
||||
(treasury, treasuryFee) = _treasuryData();
|
||||
treasuryAmount = (amount * treasuryFee) / BPS_MAX;
|
||||
}
|
||||
|
||||
uint256 adjustedAmount = amount - treasuryAmount;
|
||||
|
||||
if (referralFee != 0) {
|
||||
// The reason we levy the referral fee on the adjusted amount is so that referral fees
|
||||
// don't bypass the treasury fee, in essence referrals pay their fair share to the treasury.
|
||||
uint256 referralAmount = (adjustedAmount * referralFee) / BPS_MAX;
|
||||
adjustedAmount = adjustedAmount - referralAmount;
|
||||
|
||||
address referralRecipient = IERC721(HUB).ownerOf(referrerProfileId);
|
||||
|
||||
IERC20(currency).safeTransferFrom(collector, referralRecipient, referralAmount);
|
||||
}
|
||||
address recipient = _dataByPublicationByProfile[profileId][pubId].recipient;
|
||||
|
||||
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
|
||||
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
|
||||
}
|
||||
}
|
||||
196
contracts/core/modules/collect/LimitedFeeCollectModule.sol
Normal file
196
contracts/core/modules/collect/LimitedFeeCollectModule.sol
Normal file
@@ -0,0 +1,196 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {ICollectModule} from '../../../interfaces/ICollectModule.sol';
|
||||
import {Errors} from '../../../libraries/Errors.sol';
|
||||
import {FeeModuleBase} from '../FeeModuleBase.sol';
|
||||
import {ModuleBase} from '../ModuleBase.sol';
|
||||
import {FollowValidationModuleBase} from '../FollowValidationModuleBase.sol';
|
||||
import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
|
||||
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
|
||||
import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
|
||||
|
||||
/**
|
||||
* @notice A struct containing the necessary data to execute collect actions on a publication.
|
||||
*
|
||||
* @param collectLimit The maximum number of collects for this publication.
|
||||
* @param currentCollects The current number of collects for this publication.
|
||||
* @param amount The collecting cost associated with this publication.
|
||||
* @param recipient The recipient address associated with this publication.
|
||||
* @param currency The currency associated with this publication.
|
||||
* @param referralFee The referral fee associated with this publication.
|
||||
*/
|
||||
struct ProfilePublicationData {
|
||||
uint256 collectLimit;
|
||||
uint256 currentCollects;
|
||||
uint256 amount;
|
||||
address recipient;
|
||||
address currency;
|
||||
uint16 referralFee;
|
||||
}
|
||||
|
||||
/**
|
||||
* @title LimitedFeeCollectModule
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This is a simple Lens CollectModule implementation, inheriting from the ICollectModule interface and
|
||||
* the FeeCollectModuleBase abstract contract.
|
||||
*
|
||||
* This module works by allowing limited collects for a publication indefinitely.
|
||||
*/
|
||||
contract LimitedFeeCollectModule is ICollectModule, FeeModuleBase, FollowValidationModuleBase {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
mapping(uint256 => mapping(uint256 => ProfilePublicationData))
|
||||
internal _dataByPublicationByProfile;
|
||||
|
||||
constructor(address hub, address moduleGlobals) FeeModuleBase(moduleGlobals) ModuleBase(hub) {}
|
||||
|
||||
/**
|
||||
* @notice This collect module levies a fee on collects and supports referrals. Thus, we need to decode data.
|
||||
*
|
||||
* @param data The arbitrary data parameter, decoded into:
|
||||
* uint256 collectLimit: The maximum amount of collects.
|
||||
* uint256 amount: The currency total amount to levy.
|
||||
* address currency: The currency address, must be internally whitelisted.
|
||||
* address recipient: The custom recipient address to direct earnings to.
|
||||
* uint16 referralFee: The referral fee to set.
|
||||
*
|
||||
* @return An abi encoded bytes parameter, which is the same as the passed data parameter.
|
||||
*/
|
||||
function initializePublicationCollectModule(
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) external override onlyHub returns (bytes memory) {
|
||||
(
|
||||
uint256 collectLimit,
|
||||
uint256 amount,
|
||||
address currency,
|
||||
address recipient,
|
||||
uint16 referralFee
|
||||
) = abi.decode(data, (uint256, uint256, address, address, uint16));
|
||||
if (
|
||||
collectLimit == 0 ||
|
||||
!_currencyWhitelisted(currency) ||
|
||||
recipient == address(0) ||
|
||||
referralFee > BPS_MAX ||
|
||||
amount < BPS_MAX
|
||||
) revert Errors.InitParamsInvalid();
|
||||
|
||||
_dataByPublicationByProfile[profileId][pubId].collectLimit = collectLimit;
|
||||
_dataByPublicationByProfile[profileId][pubId].amount = amount;
|
||||
_dataByPublicationByProfile[profileId][pubId].currency = currency;
|
||||
_dataByPublicationByProfile[profileId][pubId].recipient = recipient;
|
||||
_dataByPublicationByProfile[profileId][pubId].referralFee = referralFee;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Processes a collect by:
|
||||
* 1. Ensuring the collector is a follower
|
||||
* 2. Ensuring the collect does not pass the collect limit
|
||||
* 3. Charging a fee
|
||||
*/
|
||||
function processCollect(
|
||||
uint256 referrerProfileId,
|
||||
address collector,
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) external override onlyHub {
|
||||
_checkFollowValidity(profileId, collector);
|
||||
|
||||
if (
|
||||
_dataByPublicationByProfile[profileId][pubId].currentCollects >=
|
||||
_dataByPublicationByProfile[profileId][pubId].collectLimit
|
||||
) {
|
||||
revert Errors.MintLimitExceeded();
|
||||
} else {
|
||||
_dataByPublicationByProfile[profileId][pubId].currentCollects++;
|
||||
if (referrerProfileId == profileId) {
|
||||
_processCollect(collector, profileId, pubId, data);
|
||||
} else {
|
||||
_processCollectWithReferral(referrerProfileId, collector, profileId, pubId, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Returns the publication data for a given publication, or an empty struct if that publication was not
|
||||
* initialized with this module.
|
||||
*
|
||||
* @param profileId The token ID of the profile mapped to the publication to query.
|
||||
* @param pubId The publication ID of the publication to query.
|
||||
*
|
||||
* @return The ProfilePublicationData struct mapped to that publication.
|
||||
*/
|
||||
function getPublicationData(uint256 profileId, uint256 pubId)
|
||||
external
|
||||
view
|
||||
returns (ProfilePublicationData memory)
|
||||
{
|
||||
return _dataByPublicationByProfile[profileId][pubId];
|
||||
}
|
||||
|
||||
function _processCollect(
|
||||
address collector,
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) internal {
|
||||
uint256 amount = _dataByPublicationByProfile[profileId][pubId].amount;
|
||||
address currency = _dataByPublicationByProfile[profileId][pubId].currency;
|
||||
_validateDataIsExpected(data, currency, amount);
|
||||
|
||||
(address treasury, uint16 treasuryFee) = _treasuryData();
|
||||
address recipient = _dataByPublicationByProfile[profileId][pubId].recipient;
|
||||
uint256 treasuryAmount = (amount * treasuryFee) / BPS_MAX;
|
||||
uint256 adjustedAmount = amount - treasuryAmount;
|
||||
|
||||
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
|
||||
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
|
||||
}
|
||||
|
||||
function _processCollectWithReferral(
|
||||
uint256 referrerProfileId,
|
||||
address collector,
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) internal {
|
||||
uint256 amount = _dataByPublicationByProfile[profileId][pubId].amount;
|
||||
address currency = _dataByPublicationByProfile[profileId][pubId].currency;
|
||||
_validateDataIsExpected(data, currency, amount);
|
||||
|
||||
uint256 referralFee = _dataByPublicationByProfile[profileId][pubId].referralFee;
|
||||
address treasury;
|
||||
uint256 treasuryAmount;
|
||||
|
||||
// Avoids stack too deep
|
||||
{
|
||||
uint16 treasuryFee;
|
||||
(treasury, treasuryFee) = _treasuryData();
|
||||
treasuryAmount = (amount * treasuryFee) / BPS_MAX;
|
||||
}
|
||||
|
||||
uint256 adjustedAmount = amount - treasuryAmount;
|
||||
|
||||
if (referralFee != 0) {
|
||||
// The reason we levy the referral fee on the adjusted amount is so that referral fees
|
||||
// don't bypass the treasury fee, in essence referrals pay their fair share to the treasury.
|
||||
uint256 referralAmount = (adjustedAmount * referralFee) / BPS_MAX;
|
||||
adjustedAmount = adjustedAmount - referralAmount;
|
||||
|
||||
address referralRecipient = IERC721(HUB).ownerOf(referrerProfileId);
|
||||
|
||||
IERC20(currency).safeTransferFrom(collector, referralRecipient, referralAmount);
|
||||
}
|
||||
address recipient = _dataByPublicationByProfile[profileId][pubId].recipient;
|
||||
|
||||
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
|
||||
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
|
||||
}
|
||||
}
|
||||
207
contracts/core/modules/collect/LimitedTimedFeeCollectModule.sol
Normal file
207
contracts/core/modules/collect/LimitedTimedFeeCollectModule.sol
Normal file
@@ -0,0 +1,207 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {ICollectModule} from '../../../interfaces/ICollectModule.sol';
|
||||
import {Errors} from '../../../libraries/Errors.sol';
|
||||
import {FeeModuleBase} from '../FeeModuleBase.sol';
|
||||
import {ModuleBase} from '../ModuleBase.sol';
|
||||
import {FollowValidationModuleBase} from '../FollowValidationModuleBase.sol';
|
||||
import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
|
||||
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
|
||||
import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
|
||||
|
||||
/**
|
||||
* @notice A struct containing the necessary data to execute collect actions on a publication.
|
||||
*
|
||||
* @param collectLimit The maximum number of collects for this publication.
|
||||
* @param currentCollects The current number of collects for this publication.
|
||||
* @param amount The collecting cost associated with this publication.
|
||||
* @param currency The currency associated with this publication.
|
||||
* @param recipient The recipient address associated with this publication.
|
||||
* @param referralFee The referral fee associated with this publication.
|
||||
* @param endTimestamp The end timestamp after which collecting is impossible.
|
||||
*/
|
||||
struct ProfilePublicationData {
|
||||
uint256 collectLimit;
|
||||
uint256 currentCollects;
|
||||
uint256 amount;
|
||||
address currency;
|
||||
address recipient;
|
||||
uint16 referralFee;
|
||||
uint40 endTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @title LimitedTimedFeeCollectModule
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This is a simple Lens CollectModule implementation, inheriting from the ICollectModule interface and
|
||||
* the FeeCollectModuleBase abstract contract. To optimize on gas, this module uses a constant 24 hour maximum
|
||||
* collection time.
|
||||
*
|
||||
* This module works by allowing limited collects for a publication within the allotted time with a given fee.
|
||||
*/
|
||||
contract LimitedTimedFeeCollectModule is ICollectModule, FeeModuleBase, FollowValidationModuleBase {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
uint24 internal constant ONE_DAY = 24 hours;
|
||||
|
||||
mapping(uint256 => mapping(uint256 => ProfilePublicationData))
|
||||
internal _dataByPublicationByProfile;
|
||||
|
||||
constructor(address hub, address moduleGlobals) FeeModuleBase(moduleGlobals) ModuleBase(hub) {}
|
||||
|
||||
/**
|
||||
* @notice This collect module levies a fee on collects and supports referrals. Thus, we need to decode data.
|
||||
*
|
||||
* @param data The arbitrary data parameter, decoded into:
|
||||
* uint256 collectLimit: The maximum amount of collects.
|
||||
* uint256 amount: The currency total amount to levy.
|
||||
* address currency: The currency address, must be internally whitelisted.
|
||||
* address recipient: The custom recipient address to direct earnings to.
|
||||
* uint16 referralFee: The referral fee to set.
|
||||
*
|
||||
* @return An abi encoded bytes parameter, containing (in order): collectLimit, amount, currency, recipient, referral fee & end timestamp.
|
||||
*/
|
||||
function initializePublicationCollectModule(
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) external override onlyHub returns (bytes memory) {
|
||||
uint40 endTimestamp = uint40(block.timestamp) + ONE_DAY;
|
||||
|
||||
(
|
||||
uint256 collectLimit,
|
||||
uint256 amount,
|
||||
address currency,
|
||||
address recipient,
|
||||
uint16 referralFee
|
||||
) = abi.decode(data, (uint256, uint256, address, address, uint16));
|
||||
if (
|
||||
collectLimit == 0 ||
|
||||
!_currencyWhitelisted(currency) ||
|
||||
recipient == address(0) ||
|
||||
referralFee > BPS_MAX ||
|
||||
amount < BPS_MAX
|
||||
) revert Errors.InitParamsInvalid();
|
||||
|
||||
_dataByPublicationByProfile[profileId][pubId].collectLimit = collectLimit;
|
||||
_dataByPublicationByProfile[profileId][pubId].amount = amount;
|
||||
_dataByPublicationByProfile[profileId][pubId].currency = currency;
|
||||
_dataByPublicationByProfile[profileId][pubId].recipient = recipient;
|
||||
_dataByPublicationByProfile[profileId][pubId].referralFee = referralFee;
|
||||
_dataByPublicationByProfile[profileId][pubId].endTimestamp = endTimestamp;
|
||||
|
||||
return abi.encode(collectLimit, amount, currency, recipient, referralFee, endTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Processes a collect by:
|
||||
* 1. Ensuring the collector is a follower
|
||||
* 2. Ensuring the current timestamp is less than or equal to the collect end timestamp
|
||||
* 3. Ensuring the collect does not pass the collect limit
|
||||
* 4. Charging a fee
|
||||
*/
|
||||
function processCollect(
|
||||
uint256 referrerProfileId,
|
||||
address collector,
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) external override onlyHub {
|
||||
_checkFollowValidity(profileId, collector);
|
||||
uint256 endTimestamp = _dataByPublicationByProfile[profileId][pubId].endTimestamp;
|
||||
if (block.timestamp > endTimestamp) revert Errors.CollectExpired();
|
||||
|
||||
if (
|
||||
_dataByPublicationByProfile[profileId][pubId].currentCollects >=
|
||||
_dataByPublicationByProfile[profileId][pubId].collectLimit
|
||||
) {
|
||||
revert Errors.MintLimitExceeded();
|
||||
} else {
|
||||
_dataByPublicationByProfile[profileId][pubId].currentCollects++;
|
||||
if (referrerProfileId == profileId) {
|
||||
_processCollect(collector, profileId, pubId, data);
|
||||
} else {
|
||||
_processCollectWithReferral(referrerProfileId, collector, profileId, pubId, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Returns the publication data for a given publication, or an empty struct if that publication was not
|
||||
* initialized with this module.
|
||||
*
|
||||
* @param profileId The token ID of the profile mapped to the publication to query.
|
||||
* @param pubId The publication ID of the publication to query.
|
||||
*
|
||||
* @return The ProfilePublicationData struct mapped to that publication.
|
||||
*/
|
||||
function getPublicationData(uint256 profileId, uint256 pubId)
|
||||
external
|
||||
view
|
||||
returns (ProfilePublicationData memory)
|
||||
{
|
||||
return _dataByPublicationByProfile[profileId][pubId];
|
||||
}
|
||||
|
||||
function _processCollect(
|
||||
address collector,
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) internal {
|
||||
uint256 amount = _dataByPublicationByProfile[profileId][pubId].amount;
|
||||
address currency = _dataByPublicationByProfile[profileId][pubId].currency;
|
||||
_validateDataIsExpected(data, currency, amount);
|
||||
|
||||
(address treasury, uint16 treasuryFee) = _treasuryData();
|
||||
address recipient = _dataByPublicationByProfile[profileId][pubId].recipient;
|
||||
uint256 treasuryAmount = (amount * treasuryFee) / BPS_MAX;
|
||||
uint256 adjustedAmount = amount - treasuryAmount;
|
||||
|
||||
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
|
||||
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
|
||||
}
|
||||
|
||||
function _processCollectWithReferral(
|
||||
uint256 referrerProfileId,
|
||||
address collector,
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) internal {
|
||||
uint256 amount = _dataByPublicationByProfile[profileId][pubId].amount;
|
||||
address currency = _dataByPublicationByProfile[profileId][pubId].currency;
|
||||
_validateDataIsExpected(data, currency, amount);
|
||||
|
||||
uint256 referralFee = _dataByPublicationByProfile[profileId][pubId].referralFee;
|
||||
address treasury;
|
||||
uint256 treasuryAmount;
|
||||
|
||||
// Avoids stack too deep
|
||||
{
|
||||
uint16 treasuryFee;
|
||||
(treasury, treasuryFee) = _treasuryData();
|
||||
treasuryAmount = (amount * treasuryFee) / BPS_MAX;
|
||||
}
|
||||
|
||||
uint256 adjustedAmount = amount - treasuryAmount;
|
||||
|
||||
if (referralFee != 0) {
|
||||
// The reason we levy the referral fee on the adjusted amount is so that referral fees
|
||||
// don't bypass the treasury fee, in essence referrals pay their fair share to the treasury.
|
||||
uint256 referralAmount = (adjustedAmount * referralFee) / BPS_MAX;
|
||||
adjustedAmount = adjustedAmount - referralAmount;
|
||||
|
||||
address referralRecipient = IERC721(HUB).ownerOf(referrerProfileId);
|
||||
|
||||
IERC20(currency).safeTransferFrom(collector, referralRecipient, referralAmount);
|
||||
}
|
||||
address recipient = _dataByPublicationByProfile[profileId][pubId].recipient;
|
||||
|
||||
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
|
||||
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
|
||||
}
|
||||
}
|
||||
41
contracts/core/modules/collect/RevertCollectModule.sol
Normal file
41
contracts/core/modules/collect/RevertCollectModule.sol
Normal file
@@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {ICollectModule} from '../../../interfaces/ICollectModule.sol';
|
||||
import {Errors} from '../../../libraries/Errors.sol';
|
||||
|
||||
/**
|
||||
* @title RevertCollectModule
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This is a simple Lens CollectModule implementation, inheriting from the ICollectModule interface.
|
||||
*
|
||||
* This module works by disallowing all collects.
|
||||
*/
|
||||
contract RevertCollectModule is ICollectModule {
|
||||
/**
|
||||
* @dev There is nothing needed at initialization.
|
||||
*/
|
||||
function initializePublicationCollectModule(
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) external pure override returns (bytes memory) {
|
||||
return new bytes(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Processes a collect by:
|
||||
* 1. Always reverting
|
||||
*/
|
||||
function processCollect(
|
||||
uint256 referrerProfileId,
|
||||
address collector,
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) external pure override {
|
||||
revert Errors.CollectNotAllowed();
|
||||
}
|
||||
}
|
||||
192
contracts/core/modules/collect/TimedFeeCollectModule.sol
Normal file
192
contracts/core/modules/collect/TimedFeeCollectModule.sol
Normal file
@@ -0,0 +1,192 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {ICollectModule} from '../../../interfaces/ICollectModule.sol';
|
||||
import {IFollowModule} from '../../../interfaces/IFollowModule.sol';
|
||||
import {ILensHub} from '../../../interfaces/ILensHub.sol';
|
||||
import {Errors} from '../../../libraries/Errors.sol';
|
||||
import {FeeModuleBase} from '../FeeModuleBase.sol';
|
||||
import {ModuleBase} from '../ModuleBase.sol';
|
||||
import {FollowValidationModuleBase} from '../FollowValidationModuleBase.sol';
|
||||
import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
|
||||
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
|
||||
import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
|
||||
|
||||
/**
|
||||
* @notice A struct containing the necessary data to execute collect actions on a publication.
|
||||
*
|
||||
* @param amount The collecting cost associated with this publication.
|
||||
* @param currency The currency associated with this publication.
|
||||
* @param recipient The recipient address associated with this publication.
|
||||
* @param referralFee The referral fee associated with this publication.
|
||||
* @param endTimestamp The end timestamp after which collecting is impossible.
|
||||
*/
|
||||
struct ProfilePublicationData {
|
||||
uint256 amount;
|
||||
address currency;
|
||||
address recipient;
|
||||
uint16 referralFee;
|
||||
uint40 endTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @title TimedFeeCollectModule
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This is a simple Lens CollectModule implementation, inheriting from the ICollectModule interface and
|
||||
* the FeeCollectModuleBase abstract contract. To optimize on gas, this module uses a constant 24 hour maximum
|
||||
* collection time.
|
||||
*
|
||||
* This module works by allowing unlimited collects for a publication within the allotted time with a given fee.
|
||||
*
|
||||
* NOTE: If data passed on initialization is empty, this module will only check for the time limit.
|
||||
*/
|
||||
contract TimedFeeCollectModule is ICollectModule, FeeModuleBase, FollowValidationModuleBase {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
uint24 internal constant ONE_DAY = 24 hours;
|
||||
|
||||
mapping(uint256 => mapping(uint256 => ProfilePublicationData))
|
||||
internal _dataByPublicationByProfile;
|
||||
|
||||
constructor(address hub, address moduleGlobals) FeeModuleBase(moduleGlobals) ModuleBase(hub) {}
|
||||
|
||||
/**
|
||||
* @notice This collect module levies a fee on collects and supports referrals. Thus, we need to decode data.
|
||||
*
|
||||
* @param data The arbitrary data parameter, decoded into:
|
||||
* uint256 amount: The currency total amount to levy.
|
||||
* address currency: The currency address, must be internally whitelisted.
|
||||
* address recipient: The custom recipient address to direct earnings to.
|
||||
* uint16 referralFee: The referral fee to set.
|
||||
*
|
||||
* @return An abi encoded bytes parameter, containing (in order): amount, currency, recipient, referral fee & end timestamp.
|
||||
*/
|
||||
function initializePublicationCollectModule(
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) external override onlyHub returns (bytes memory) {
|
||||
uint40 endTimestamp = uint40(block.timestamp) + ONE_DAY;
|
||||
|
||||
(uint256 amount, address currency, address recipient, uint16 referralFee) = abi.decode(
|
||||
data,
|
||||
(uint256, address, address, uint16)
|
||||
);
|
||||
if (
|
||||
!_currencyWhitelisted(currency) ||
|
||||
recipient == address(0) ||
|
||||
referralFee > BPS_MAX ||
|
||||
amount < BPS_MAX
|
||||
) revert Errors.InitParamsInvalid();
|
||||
|
||||
_dataByPublicationByProfile[profileId][pubId].amount = amount;
|
||||
_dataByPublicationByProfile[profileId][pubId].currency = currency;
|
||||
_dataByPublicationByProfile[profileId][pubId].recipient = recipient;
|
||||
_dataByPublicationByProfile[profileId][pubId].referralFee = referralFee;
|
||||
_dataByPublicationByProfile[profileId][pubId].endTimestamp = endTimestamp;
|
||||
|
||||
return abi.encode(amount, currency, recipient, referralFee, endTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Processes a collect by:
|
||||
* 1. Ensuring the collector is a follower
|
||||
* 2. Ensuring the current timestamp is less than or equal to the collect end timestamp
|
||||
* 3. Charging a fee
|
||||
*/
|
||||
function processCollect(
|
||||
uint256 referrerProfileId,
|
||||
address collector,
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) external override onlyHub {
|
||||
_checkFollowValidity(profileId, collector);
|
||||
uint256 endTimestamp = _dataByPublicationByProfile[profileId][pubId].endTimestamp;
|
||||
if (block.timestamp > endTimestamp) revert Errors.CollectExpired();
|
||||
|
||||
if (referrerProfileId == profileId) {
|
||||
_processCollect(collector, profileId, pubId, data);
|
||||
} else {
|
||||
_processCollectWithReferral(referrerProfileId, collector, profileId, pubId, data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Returns the publication data for a given publication, or an empty struct if that publication was not
|
||||
* initialized with this module.
|
||||
*
|
||||
* @param profileId The token ID of the profile mapped to the publication to query.
|
||||
* @param pubId The publication ID of the publication to query.
|
||||
*
|
||||
* @return The ProfilePublicationData struct mapped to that publication.
|
||||
*/
|
||||
function getPublicationData(uint256 profileId, uint256 pubId)
|
||||
external
|
||||
view
|
||||
returns (ProfilePublicationData memory)
|
||||
{
|
||||
return _dataByPublicationByProfile[profileId][pubId];
|
||||
}
|
||||
|
||||
function _processCollect(
|
||||
address collector,
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) internal {
|
||||
uint256 amount = _dataByPublicationByProfile[profileId][pubId].amount;
|
||||
address currency = _dataByPublicationByProfile[profileId][pubId].currency;
|
||||
_validateDataIsExpected(data, currency, amount);
|
||||
|
||||
(address treasury, uint16 treasuryFee) = _treasuryData();
|
||||
address recipient = _dataByPublicationByProfile[profileId][pubId].recipient;
|
||||
uint256 treasuryAmount = (amount * treasuryFee) / BPS_MAX;
|
||||
uint256 adjustedAmount = amount - treasuryAmount;
|
||||
|
||||
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
|
||||
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
|
||||
}
|
||||
|
||||
function _processCollectWithReferral(
|
||||
uint256 referrerProfileId,
|
||||
address collector,
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) internal {
|
||||
uint256 amount = _dataByPublicationByProfile[profileId][pubId].amount;
|
||||
address currency = _dataByPublicationByProfile[profileId][pubId].currency;
|
||||
_validateDataIsExpected(data, currency, amount);
|
||||
|
||||
uint256 referralFee = _dataByPublicationByProfile[profileId][pubId].referralFee;
|
||||
address treasury;
|
||||
uint256 treasuryAmount;
|
||||
|
||||
// Avoids stack too deep
|
||||
{
|
||||
uint16 treasuryFee;
|
||||
(treasury, treasuryFee) = _treasuryData();
|
||||
treasuryAmount = (amount * treasuryFee) / BPS_MAX;
|
||||
}
|
||||
|
||||
uint256 adjustedAmount = amount - treasuryAmount;
|
||||
|
||||
if (referralFee != 0) {
|
||||
// The reason we levy the referral fee on the adjusted amount is so that referral fees
|
||||
// don't bypass the treasury fee, in essence referrals pay their fair share to the treasury.
|
||||
uint256 referralAmount = (adjustedAmount * referralFee) / BPS_MAX;
|
||||
adjustedAmount = adjustedAmount - referralAmount;
|
||||
|
||||
address referralRecipient = IERC721(HUB).ownerOf(referrerProfileId);
|
||||
|
||||
IERC20(currency).safeTransferFrom(collector, referralRecipient, referralAmount);
|
||||
}
|
||||
address recipient = _dataByPublicationByProfile[profileId][pubId].recipient;
|
||||
|
||||
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
|
||||
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
|
||||
}
|
||||
}
|
||||
133
contracts/core/modules/follow/ApprovalFollowModule.sol
Normal file
133
contracts/core/modules/follow/ApprovalFollowModule.sol
Normal file
@@ -0,0 +1,133 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {IFollowModule} from '../../../interfaces/IFollowModule.sol';
|
||||
import {Errors} from '../../../libraries/Errors.sol';
|
||||
import {Events} from '../../../libraries/Events.sol';
|
||||
import {ModuleBase} from '../ModuleBase.sol';
|
||||
import {FollowValidatorFollowModuleBase} from './FollowValidatorFollowModuleBase.sol';
|
||||
import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
|
||||
|
||||
/**
|
||||
* @title ApprovalFollowModule
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This follow module only allows addresses that are approved for a profile by the profile owner to follow.
|
||||
*/
|
||||
contract ApprovalFollowModule is IFollowModule, FollowValidatorFollowModuleBase {
|
||||
// We use a triple nested mapping so that, on profile transfer, the previous approved address list is invalid;
|
||||
mapping(address => mapping(uint256 => mapping(address => bool)))
|
||||
internal _approvedByProfileByOwner;
|
||||
|
||||
constructor(address hub) ModuleBase(hub) {}
|
||||
|
||||
/**
|
||||
* @notice A custom function that allows profile owners to customize approved addresses.
|
||||
*
|
||||
* @param profileId The profile ID to approve/disapprove follower addresses for.
|
||||
* @param addresses The addresses to approve/disapprove for following the profile.
|
||||
* @param toApprove Whether to approve or disapprove the addresses for following the profile.
|
||||
*/
|
||||
function approve(
|
||||
uint256 profileId,
|
||||
address[] calldata addresses,
|
||||
bool[] calldata toApprove
|
||||
) external {
|
||||
if (addresses.length != toApprove.length) revert Errors.InitParamsInvalid();
|
||||
address owner = IERC721(HUB).ownerOf(profileId);
|
||||
if (msg.sender != owner) revert Errors.NotProfileOwner();
|
||||
|
||||
for (uint256 i = 0; i < addresses.length; i++) {
|
||||
_approvedByProfileByOwner[owner][profileId][addresses[i]] = toApprove[i];
|
||||
}
|
||||
|
||||
emit Events.FollowsApproved(owner, profileId, addresses, toApprove, block.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice This follow module works on custom profile owner approvals.
|
||||
*
|
||||
* @param data The arbitrary data parameter, decoded into:
|
||||
* address[] addresses: The array of addresses to approve initially.
|
||||
*
|
||||
* @return An abi encoded bytes parameter, which is the same as the passed data parameter.
|
||||
*/
|
||||
function initializeFollowModule(uint256 profileId, bytes calldata data)
|
||||
external
|
||||
override
|
||||
onlyHub
|
||||
returns (bytes memory)
|
||||
{
|
||||
address owner = IERC721(HUB).ownerOf(profileId);
|
||||
|
||||
if (data.length > 0) {
|
||||
address[] memory addresses = abi.decode(data, (address[]));
|
||||
for (uint256 i = 0; i < addresses.length; i++) {
|
||||
_approvedByProfileByOwner[owner][profileId][addresses[i]] = true;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Processes a follow by:
|
||||
* 1. Validating that the follower has been approved for that profile by the profile owner
|
||||
*/
|
||||
function processFollow(
|
||||
address follower,
|
||||
uint256 profileId,
|
||||
bytes calldata data
|
||||
) external override onlyHub {
|
||||
address owner = IERC721(HUB).ownerOf(profileId);
|
||||
if (!_approvedByProfileByOwner[owner][profileId][follower])
|
||||
revert Errors.FollowNotApproved();
|
||||
_approvedByProfileByOwner[owner][profileId][follower] = false; // prevents repeat follows
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev We don't need to execute any additional logic on transfers in this follow module.
|
||||
*/
|
||||
function followModuleTransferHook(
|
||||
uint256 profileId,
|
||||
address from,
|
||||
address to,
|
||||
uint256 followNFTTokenId
|
||||
) external override {}
|
||||
|
||||
/**
|
||||
* @notice Returns whether the given address is approved for the profile owned by a given address.
|
||||
*
|
||||
* @param profileOwner The profile owner of the profile to query the approval with.
|
||||
* @param profileId The token ID of the profile to query approval with.
|
||||
* @param toCheck The address to query approval for.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
function isApproved(
|
||||
address profileOwner,
|
||||
uint256 profileId,
|
||||
address toCheck
|
||||
) external view returns (bool) {
|
||||
return _approvedByProfileByOwner[profileOwner][profileId][toCheck];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Returns whether the given addresses are approved for the profile owned by a given address.
|
||||
*
|
||||
* @param profileOwner The profile owner of the profile to query the approvals with.
|
||||
* @param profileId The token ID of the profile to query approvals with.
|
||||
* @param toCheck The address array to query approvals for.
|
||||
*/
|
||||
function isApprovedArray(
|
||||
address profileOwner,
|
||||
uint256 profileId,
|
||||
address[] calldata toCheck
|
||||
) external view returns (bool[] memory) {
|
||||
bool[] memory approved = new bool[](toCheck.length);
|
||||
for (uint256 i = 0; i < toCheck.length; i++) {
|
||||
approved[i] = _approvedByProfileByOwner[profileOwner][profileId][toCheck[i]];
|
||||
}
|
||||
return approved;
|
||||
}
|
||||
}
|
||||
114
contracts/core/modules/follow/FeeFollowModule.sol
Normal file
114
contracts/core/modules/follow/FeeFollowModule.sol
Normal file
@@ -0,0 +1,114 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {IFollowModule} from '../../../interfaces/IFollowModule.sol';
|
||||
import {ILensHub} from '../../../interfaces/ILensHub.sol';
|
||||
import {Errors} from '../../../libraries/Errors.sol';
|
||||
import {FeeModuleBase} from '../FeeModuleBase.sol';
|
||||
import {ModuleBase} from '../ModuleBase.sol';
|
||||
import {FollowValidatorFollowModuleBase} from './FollowValidatorFollowModuleBase.sol';
|
||||
import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
|
||||
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
|
||||
import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
|
||||
|
||||
/**
|
||||
* @notice A struct containing the necessary data to execute follow actions on a given profile.
|
||||
*
|
||||
* @param currency The currency associated with this profile.
|
||||
* @param amount The following cost associated with this profile.
|
||||
* @param recipient The recipient address associated with this profile.
|
||||
*/
|
||||
struct ProfileData {
|
||||
address currency;
|
||||
uint256 amount;
|
||||
address recipient;
|
||||
}
|
||||
|
||||
/**
|
||||
* @title FeeFollowModule
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This is a simple Lens FollowModule implementation, inheriting from the IFollowModule interface, but with additional
|
||||
* variables that can be controlled by governance, such as the governance & treasury addresses as well as the treasury fee.
|
||||
*/
|
||||
contract FeeFollowModule is IFollowModule, FeeModuleBase, FollowValidatorFollowModuleBase {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
mapping(uint256 => ProfileData) internal _dataByProfile;
|
||||
|
||||
constructor(address hub, address moduleGlobals) FeeModuleBase(moduleGlobals) ModuleBase(hub) {}
|
||||
|
||||
/**
|
||||
* @notice This follow module levies a fee on follows.
|
||||
*
|
||||
* @param data The arbitrary data parameter, decoded into:
|
||||
* address currency: The currency address, must be internally whitelisted.
|
||||
* uint256 amount: The currency total amount to levy.
|
||||
* address recipient: The custom recipient address to direct earnings to.
|
||||
*
|
||||
* @return An abi encoded bytes parameter, which is the same as the passed data parameter.
|
||||
*/
|
||||
function initializeFollowModule(uint256 profileId, bytes calldata data)
|
||||
external
|
||||
override
|
||||
onlyHub
|
||||
returns (bytes memory)
|
||||
{
|
||||
(uint256 amount, address currency, address recipient) = abi.decode(
|
||||
data,
|
||||
(uint256, address, address)
|
||||
);
|
||||
if (!_currencyWhitelisted(currency) || recipient == address(0) || amount < BPS_MAX)
|
||||
revert Errors.InitParamsInvalid();
|
||||
|
||||
_dataByProfile[profileId].amount = amount;
|
||||
_dataByProfile[profileId].currency = currency;
|
||||
_dataByProfile[profileId].recipient = recipient;
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Processes a follow by:
|
||||
* 1. Charging a fee
|
||||
*/
|
||||
function processFollow(
|
||||
address follower,
|
||||
uint256 profileId,
|
||||
bytes calldata data
|
||||
) external override onlyHub {
|
||||
uint256 amount = _dataByProfile[profileId].amount;
|
||||
address currency = _dataByProfile[profileId].currency;
|
||||
_validateDataIsExpected(data, currency, amount);
|
||||
|
||||
(address treasury, uint16 treasuryFee) = _treasuryData();
|
||||
address recipient = _dataByProfile[profileId].recipient;
|
||||
uint256 treasuryAmount = (amount * treasuryFee) / BPS_MAX;
|
||||
uint256 adjustedAmount = amount - treasuryAmount;
|
||||
|
||||
IERC20(currency).safeTransferFrom(follower, recipient, adjustedAmount);
|
||||
IERC20(currency).safeTransferFrom(follower, treasury, treasuryAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev We don't need to execute any additional logic on transfers in this follow module.
|
||||
*/
|
||||
function followModuleTransferHook(
|
||||
uint256 profileId,
|
||||
address from,
|
||||
address to,
|
||||
uint256 followNFTTokenId
|
||||
) external override {}
|
||||
|
||||
/**
|
||||
* @notice Returns the profile data for a given profile, or an empty struct if that profile was not initialized
|
||||
* with this module.
|
||||
*
|
||||
* @param profileId The token ID of the profile to query.
|
||||
*
|
||||
* @return The ProfileData struct mapped to that profile.
|
||||
*/
|
||||
function getProfileData(uint256 profileId) external view returns (ProfileData memory) {
|
||||
return _dataByProfile[profileId];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {IFollowModule} from '../../../interfaces/IFollowModule.sol';
|
||||
import {ILensHub} from '../../../interfaces/ILensHub.sol';
|
||||
import {Errors} from '../../../libraries/Errors.sol';
|
||||
import {ModuleBase} from '../ModuleBase.sol';
|
||||
import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
|
||||
|
||||
/**
|
||||
* @title FollowValidatorFollowModuleBase
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This abstract contract adds the default expected behavior for follow validation in a follow module
|
||||
* to inheriting contracts.
|
||||
*/
|
||||
abstract contract FollowValidatorFollowModuleBase is IFollowModule, ModuleBase {
|
||||
/**
|
||||
* @notice Standard function to validate follow NFT ownership. This module is agnostic to follow NFT token IDs
|
||||
* and other properties.
|
||||
*/
|
||||
///@inheritdoc IFollowModule
|
||||
function validateFollow(
|
||||
uint256 profileId,
|
||||
address follower,
|
||||
uint256 followNFTTokenId
|
||||
) external view override {
|
||||
address followNFT = ILensHub(HUB).getFollowNFT(profileId);
|
||||
if (followNFT == address(0)) revert Errors.FollowInvalid();
|
||||
if (followNFTTokenId == 0) {
|
||||
// check that follower owns a followNFT
|
||||
if (IERC721(followNFT).balanceOf(follower) == 0) revert Errors.FollowInvalid();
|
||||
} else {
|
||||
// check that follower owns the specific followNFT
|
||||
if (IERC721(followNFT).ownerOf(followNFTTokenId) != follower)
|
||||
revert Errors.FollowInvalid();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {IReferenceModule} from '../../../interfaces/IReferenceModule.sol';
|
||||
import {ModuleBase} from '../ModuleBase.sol';
|
||||
import {FollowValidationModuleBase} from '../FollowValidationModuleBase.sol';
|
||||
import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
|
||||
|
||||
/**
|
||||
* @title FollowerOnlyReferenceModule
|
||||
* @author Lens
|
||||
*
|
||||
* @notice A simple reference module that validates that comments or mirrors originate from a profile owned
|
||||
* by a follower.
|
||||
*/
|
||||
contract FollowerOnlyReferenceModule is IReferenceModule, FollowValidationModuleBase {
|
||||
constructor(address hub) ModuleBase(hub) {}
|
||||
|
||||
/**
|
||||
* @dev There is nothing needed at initialization.
|
||||
*/
|
||||
function initializeReferenceModule(
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) external pure override returns (bytes memory) {
|
||||
return new bytes(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Validates that the commenting profile's owner is a follower.
|
||||
*
|
||||
* NOTE: We don't need to care what the pointed publication is in this context.
|
||||
*/
|
||||
function processComment(
|
||||
uint256 profileId,
|
||||
uint256 profileIdPointed,
|
||||
uint256 pubIdPointed
|
||||
) external view override {
|
||||
address commentCreator = IERC721(HUB).ownerOf(profileId);
|
||||
_checkFollowValidity(profileIdPointed, commentCreator);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Validates that the commenting profile's owner is a follower.
|
||||
*
|
||||
* NOTE: We don't need to care what the pointed publication is in this context.
|
||||
*/
|
||||
function processMirror(
|
||||
uint256 profileId,
|
||||
uint256 profileIdPointed,
|
||||
uint256 pubIdPointed
|
||||
) external view override {
|
||||
address mirrorCreator = IERC721(HUB).ownerOf(profileId);
|
||||
_checkFollowValidity(profileIdPointed, mirrorCreator);
|
||||
}
|
||||
}
|
||||
72
contracts/core/storage/LensHubStorage.sol
Normal file
72
contracts/core/storage/LensHubStorage.sol
Normal file
@@ -0,0 +1,72 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {DataTypes} from '../../libraries/DataTypes.sol';
|
||||
|
||||
contract LensHubStorage {
|
||||
bytes32 internal constant CREATE_PROFILE_WITH_SIG_TYPEHASH =
|
||||
0x9ac3269d9abd6f8c5e850e07f21b199079e8a5cc4a55466d8c96ab0c4a5be403;
|
||||
// keccak256(
|
||||
// 'CreateProfileWithSig(string handle,string uri,address followModule,bytes followModuleData,uint256 nonce,uint256 deadline)'
|
||||
// );
|
||||
bytes32 internal constant SET_FOLLOW_MODULE_WITH_SIG_TYPEHASH =
|
||||
0x6f3f6455a608af1cc57ef3e5c0a49deeb88bba264ec8865b798ff07358859d4b;
|
||||
// keccak256(
|
||||
// 'SetFollowModuleWithSig(uint256 profileId,address followModule,bytes followModuleData,uint256 nonce,uint256 deadline)'
|
||||
// );
|
||||
bytes32 internal constant SET_FOLLOW_NFT_URI_WITH_SIG_TYPEHASH =
|
||||
0xd8d76e8b2b26e1ebe72def13fb559a68561ef064055b0de01f955bc26e25d42f;
|
||||
// keccak256(
|
||||
// 'SetFollowNFTURIWithSig(uint256 profileId,string followNFTURI,uint256 nonce,uint256 deadline)'
|
||||
// );
|
||||
bytes32 internal constant SET_DISPATCHER_WITH_SIG_TYPEHASH =
|
||||
0x77ba3e9f5fa75343bbad1241fb539a0064de97694b47d463d1eb5c54aba11f0f;
|
||||
// keccak256(
|
||||
// 'SetDispatcherWithSig(uint256 profileId,address dispatcher,uint256 nonce,uint256 deadline)'
|
||||
// );
|
||||
bytes32 internal constant SET_PROFILE_IMAGE_URI_WITH_SIG_TYPEHASH =
|
||||
0x5b9860bd835e648945b22d053515bc1f53b7d9fab4b23b1b49db15722e945d14;
|
||||
// keccak256(
|
||||
// 'SetProfileImageURIWithSig(uint256 profileId,string imageURI,uint256 nonce,uint256 deadline)'
|
||||
// );
|
||||
bytes32 internal constant POST_WITH_SIG_TYPEHASH =
|
||||
0xfb8f057542e7551386ead0b891a45f102af78c47f8cc58b4a919c7cfeccd0e1e;
|
||||
// keccak256(
|
||||
// 'PostWithSig(uint256 profileId,string contentURI,address collectModule,bytes collectModuleData,address referenceModule,bytes referenceModuleData,uint256 nonce,uint256 deadline)'
|
||||
// );
|
||||
bytes32 internal constant COMMENT_WITH_SIG_TYPEHASH =
|
||||
0xb30910150df56294e05b2d03e181803697a2b935abb1b9bdddde9310f618fe9b;
|
||||
// keccak256(
|
||||
// 'CommentWithSig(uint256 profileId,string contentURI,uint256 profileIdPointed,uint256 pubIdPointed,address collectModule,bytes collectModuleData,address referenceModule,bytes referenceModuleData,uint256 nonce,uint256 deadline)'
|
||||
// );
|
||||
bytes32 internal constant MIRROR_WITH_SIG_TYPEHASH =
|
||||
0x64f4578fc098f96a2450fbe601cb8c5318ebeb2ff72d2031a36be1ff6932d5ee;
|
||||
// keccak256(
|
||||
// 'MirrorWithSig(uint256 profileId,uint256 profileIdPointed,uint256 pubIdPointed,address referenceModule,bytes referenceModuleData,uint256 nonce,uint256 deadline)'
|
||||
// );
|
||||
bytes32 internal constant FOLLOW_WITH_SIG_TYPEHASH =
|
||||
0xfb6b7f1cd1b38daf3822aff0abbe78124db5d62a4748bcff007c15ccd6d30bc5;
|
||||
// keccak256(
|
||||
// 'FollowWithSig(uint256[] profileIds,bytes[] datas,uint256 nonce,uint256 deadline)'
|
||||
// );
|
||||
bytes32 internal constant COLLECT_WITH_SIG_TYPEHASH =
|
||||
0x7f9b4ea1fc678b4fda1611ac5cbd28f339e235d89b1540635e9b2e0223a3c101;
|
||||
// keccak256(
|
||||
// 'CollectWithSig(uint256 profileId,uint256 pubId,bytes data,uint256 nonce,uint256 deadline)'
|
||||
// );
|
||||
|
||||
mapping(address => bool) internal _profileCreatorWhitelisted;
|
||||
mapping(address => bool) internal _followModuleWhitelisted;
|
||||
mapping(address => bool) internal _collectModuleWhitelisted;
|
||||
mapping(address => bool) internal _referenceModuleWhitelisted;
|
||||
|
||||
mapping(uint256 => address) internal _dispatcherByProfile;
|
||||
mapping(bytes32 => uint256) internal _profileIdByHandleHash;
|
||||
mapping(uint256 => DataTypes.ProfileStruct) internal _profileById;
|
||||
mapping(uint256 => mapping(uint256 => DataTypes.PublicationStruct)) internal _pubByIdByProfile;
|
||||
|
||||
uint256 internal _profileCounter;
|
||||
address internal _governance;
|
||||
address internal _emergencyAdmin;
|
||||
}
|
||||
44
contracts/interfaces/ICollectModule.sol
Normal file
44
contracts/interfaces/ICollectModule.sol
Normal file
@@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
/**
|
||||
* @title ICollectModule
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This is the standard interface for all Lens-compatible CollectModules.
|
||||
*/
|
||||
interface ICollectModule {
|
||||
/**
|
||||
* @notice Initializes data for a given publication being published. This can only be called by the hub.
|
||||
*
|
||||
* @param profileId The token ID of the profile publishing the publication.
|
||||
* @param pubId The associated publication's LensHub publication ID.
|
||||
* @param data Arbitrary data __passed from the user!__ to be decoded.
|
||||
*
|
||||
* @return An abi encoded byte array encapsulating the execution's state changes. This will be emitted by the
|
||||
* hub alongside the collect module's address and should be consumed by front ends.
|
||||
*/
|
||||
function initializePublicationCollectModule(
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) external returns (bytes memory);
|
||||
|
||||
/**
|
||||
* @notice Processes a collect action for a given publication, this can only be called by the hub.
|
||||
*
|
||||
* @param referrerProfileId The LensHub profile token ID of the referrer's profile (only different in case of mirrors).
|
||||
* @param collector The collector address.
|
||||
* @param profileId The token ID of the profile associated with the publication being collected.
|
||||
* @param pubId The LensHub publication ID associated with the publication being collected.
|
||||
* @param data Arbitrary data __passed from the collector!__ to be decoded.
|
||||
*/
|
||||
function processCollect(
|
||||
uint256 referrerProfileId,
|
||||
address collector,
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) external;
|
||||
}
|
||||
43
contracts/interfaces/ICollectNFT.sol
Normal file
43
contracts/interfaces/ICollectNFT.sol
Normal file
@@ -0,0 +1,43 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
/**
|
||||
* @title ICollectNFT
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This is the interface for the CollectNFT contract. Which is cloned upon the first collect for any given
|
||||
* publication.
|
||||
*/
|
||||
interface ICollectNFT {
|
||||
/**
|
||||
* @notice Initializes the collect NFT, setting the feed as the privileged minter, storing the collected publication pointer
|
||||
* and initializing the name and symbol in the LensNFTBase contract.
|
||||
*
|
||||
* @param profileId The token ID of the profile in the hub that this collectNFT points to.
|
||||
* @param pubId The profile publication ID in the hub that this collectNFT 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;
|
||||
|
||||
/**
|
||||
* @notice Mints a collect NFT to the specified address. This can only be called by the hub, and is called
|
||||
* upon collection.
|
||||
*
|
||||
* @param to The address to mint the NFT to.
|
||||
*/
|
||||
function mint(address to) external;
|
||||
|
||||
/**
|
||||
* @notice Returns the source publication pointer mapped to this collect NFT.
|
||||
*
|
||||
* @return First the profile ID uint256, and second the pubId uint256.
|
||||
*/
|
||||
function getSourcePublicationPointer() external view returns (uint256, uint256);
|
||||
}
|
||||
80
contracts/interfaces/IFollowModule.sol
Normal file
80
contracts/interfaces/IFollowModule.sol
Normal file
@@ -0,0 +1,80 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
/**
|
||||
* @title IFollowModule
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This is the standard interface for all Lens-compatible FollowModules.
|
||||
*/
|
||||
interface IFollowModule {
|
||||
/**
|
||||
* @notice Initializes a follow module for a given Lens profile. This can only be called by the hub contract.
|
||||
*
|
||||
* @param profileId The token ID of the profile to initialize this follow module for.
|
||||
* @param data Arbitrary data passed by the profile creator.
|
||||
*/
|
||||
function initializeFollowModule(uint256 profileId, bytes calldata data)
|
||||
external
|
||||
returns (bytes memory);
|
||||
|
||||
/**
|
||||
* @notice Processes a given follow, this can only be called from the LensHub contract.
|
||||
*
|
||||
* @param follower The follower address.
|
||||
* @param profileId The token ID of the profile being followed.
|
||||
* @param data Arbitrary data passed by the follower.
|
||||
*/
|
||||
function processFollow(
|
||||
address follower,
|
||||
uint256 profileId,
|
||||
bytes calldata data
|
||||
) external;
|
||||
|
||||
/**
|
||||
* @notice This is a transfer hook that is called upon follow NFT transfer in `beforeTokenTransfer. This can
|
||||
* only be called from the LensHub contract.
|
||||
*
|
||||
* NOTE: Special care needs to be taken here: It is possible that follow NFTs were issued before this module
|
||||
* was initialized if the profile's follow module was previously different. This transfer hook should take this
|
||||
* into consideration, especially when the module holds state associated with individual follow NFTs.
|
||||
*
|
||||
* @param profileId The token ID of the profile associated with the follow NFT being transferred.
|
||||
* @param from The address sending the follow NFT.
|
||||
* @param to The address receiving the follow NFT.
|
||||
* @param followNFTTokenId The token ID of the follow NFT being transferred.
|
||||
*/
|
||||
function followModuleTransferHook(
|
||||
uint256 profileId,
|
||||
address from,
|
||||
address to,
|
||||
uint256 followNFTTokenId
|
||||
) external;
|
||||
|
||||
/**
|
||||
* @notice This is a helper function that could be used in conjunction with specific collect modules.
|
||||
*
|
||||
* NOTE: This function IS meant to replace a check on follower NFT ownership.
|
||||
*
|
||||
* NOTE: It is assumed that not all collect modules are aware of the token ID to pass. In these cases,
|
||||
* this should receive a `followNFTTokenId` of 0, which is impossible regardless.
|
||||
*
|
||||
* One example of a use case for this would be a subscription-based following system:
|
||||
* 1. The collect module:
|
||||
* - Decodes a follower NFT token ID from user-passed data.
|
||||
* - Fetches the follow module from the hub.
|
||||
* - Calls `validateFollow` passing the profile ID, follower & follower token ID.
|
||||
* 2. The follow module:
|
||||
* - Validates the subscription status for that given NFT, reverting on an invalid subscription.
|
||||
*
|
||||
* @param profileId The token ID of the profile to validate the follow for.
|
||||
* @param follower The follower address to validate the follow for.
|
||||
* @param followNFTTokenId The followNFT token ID to validate the follow for.
|
||||
*/
|
||||
function validateFollow(
|
||||
uint256 profileId,
|
||||
address follower,
|
||||
uint256 followNFTTokenId
|
||||
) external view;
|
||||
}
|
||||
63
contracts/interfaces/IFollowNFT.sol
Normal file
63
contracts/interfaces/IFollowNFT.sol
Normal file
@@ -0,0 +1,63 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {DataTypes} from '../libraries/DataTypes.sol';
|
||||
|
||||
/**
|
||||
* @title IFollowNFT
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This is the interface for the FollowNFT contract, which is cloned upon the first follow for any profile.
|
||||
*/
|
||||
interface IFollowNFT {
|
||||
/**
|
||||
* @notice Initializes the follow NFT, setting the feed as the privileged minter, initializing the name and
|
||||
* symbol in the LensNFTBase contract.
|
||||
*
|
||||
* @param profileId The token ID of the profile in the hub associated with this followNFT, used for transfer hooks.
|
||||
* @param name The name to set for this NFT.
|
||||
* @param symbol The symbol to set for this NFT.
|
||||
*/
|
||||
function initialize(
|
||||
uint256 profileId,
|
||||
string calldata name,
|
||||
string calldata symbol
|
||||
) external;
|
||||
|
||||
/**
|
||||
* @notice Mints a follow NFT to the specified address. This can only be called by the hub, and is called
|
||||
* upon follow.
|
||||
*
|
||||
* @param to The address to mint the NFT to.
|
||||
*/
|
||||
function mint(address to) external;
|
||||
|
||||
/**
|
||||
* @notice Delegates the caller's governance power to the given delegatee address.
|
||||
*
|
||||
* @param delegatee The delegatee address to delegate governance power to.
|
||||
*/
|
||||
function delegate(address delegatee) external;
|
||||
|
||||
/**
|
||||
* @notice Delegates the delegator's governance power via meta-tx to the given delegatee address.
|
||||
*
|
||||
* @param delegator The delegator address, who is the signer.
|
||||
* @param delegatee The delegatee address, who is receiving the governance power delegation.
|
||||
* @param sig The EIP712Signature struct containing the necessary parameters to recover the delegator's signature.
|
||||
*/
|
||||
function delegateBySig(
|
||||
address delegator,
|
||||
address delegatee,
|
||||
DataTypes.EIP712Signature calldata sig
|
||||
) external;
|
||||
|
||||
/**
|
||||
* @notice Returns the governance power for a given user at a specified block number.
|
||||
*
|
||||
* @param user The user to query governance power for.
|
||||
* @param blockNumber The block number to query the user's governance power at.
|
||||
*/
|
||||
function getPowerByBlockNumber(address user, uint256 blockNumber) external returns (uint256);
|
||||
}
|
||||
478
contracts/interfaces/ILensHub.sol
Normal file
478
contracts/interfaces/ILensHub.sol
Normal file
@@ -0,0 +1,478 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {DataTypes} from '../libraries/DataTypes.sol';
|
||||
|
||||
/**
|
||||
* @title ILensHub
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This is the interface for the LensHub contract, the main entry point for the Lens protocol.
|
||||
* You'll find all the events and external functions, as well as the reasoning behind them here.
|
||||
*/
|
||||
interface ILensHub {
|
||||
/**
|
||||
* @notice Initializes the LensHub NFT, setting the initial governance address as well as the name and symbol in
|
||||
* the LensNFTBase contract.
|
||||
*
|
||||
* @param name The name to set for the hub NFT.
|
||||
* @param symbol The symbol to set for the hub NFT.
|
||||
* @param newGovernance The governance address to set.
|
||||
*/
|
||||
function initialize(
|
||||
string calldata name,
|
||||
string calldata symbol,
|
||||
address newGovernance
|
||||
) external;
|
||||
|
||||
/**
|
||||
* @notice Sets the privileged governance role. This function can only be called by the current governance
|
||||
* address.
|
||||
*
|
||||
* @param newGovernance The new governance address to set.
|
||||
*/
|
||||
function setGovernance(address newGovernance) external;
|
||||
|
||||
/**
|
||||
* @notice Sets the emergency admin, which is a permissioned role able to set the protocol state. This function
|
||||
* can only be called by the governance address.
|
||||
*
|
||||
* @param newEmergencyAdmin The new emergency admin address to set.
|
||||
*/
|
||||
function setEmergencyAdmin(address newEmergencyAdmin) external;
|
||||
|
||||
/**
|
||||
* @notice Sets the protocol state to either a global pause, a publishing pause or an unpaused state. This function
|
||||
* can only be called by the governance address or the emergency admin address.
|
||||
*
|
||||
* @param state The state to set, as a member of the ProtocolState enum.
|
||||
*/
|
||||
function setState(DataTypes.ProtocolState state) external;
|
||||
|
||||
/**
|
||||
* @notice Adds or removes a profile creator from the whitelist. This function can only be called by the current
|
||||
* governance address.
|
||||
*
|
||||
* @param profileCreator The profile creator address to add or remove from the whitelist.
|
||||
* @param whitelist Whether or not the profile creator should be whitelisted.
|
||||
*/
|
||||
function whitelistProfileCreator(address profileCreator, bool whitelist) external;
|
||||
|
||||
/**
|
||||
* @notice Adds or removes a follow module from the whitelist. This function can only be called by the current
|
||||
* governance address.
|
||||
*
|
||||
* @param followModule The follow module contract address to add or remove from the whitelist.
|
||||
* @param whitelist Whether or not the follow module should be whitelisted.
|
||||
*/
|
||||
function whitelistFollowModule(address followModule, bool whitelist) external;
|
||||
|
||||
/**
|
||||
* @notice Adds or removes a reference module from the whitelist. This function can only be called by the current
|
||||
* governance address.
|
||||
*
|
||||
* @param referenceModule The reference module contract to add or remove from the whitelist.
|
||||
* @param whitelist Whether or not the reference module should be whitelisted.
|
||||
*/
|
||||
function whitelistReferenceModule(address referenceModule, bool whitelist) external;
|
||||
|
||||
/**
|
||||
* @notice Adds or removes a collect module from the whitelist. This function can only be called by the current
|
||||
* governance address.
|
||||
*
|
||||
* @param collectModule The collect module contract address to add or remove from the whitelist.
|
||||
* @param whitelist Whether or not the collect module should be whitelisted.
|
||||
*/
|
||||
function whitelistCollectModule(address collectModule, bool whitelist) external;
|
||||
|
||||
/**
|
||||
* @notice Creates a profile with the specified parameters, minting a profile NFT to the given recipient. This
|
||||
* function must be called by a whitelisted profile creator.
|
||||
*
|
||||
* @param vars A CreateProfileData struct containing the following params:
|
||||
* to: The address receiving the profile.
|
||||
* handle: The handle to set for the profile, must be unique and non-empty.
|
||||
* imageURI: The URI to set for the profile image.
|
||||
* followModule: The follow module to use, can be the zero address.
|
||||
* followModuleData: The follow module initialization data, if any
|
||||
*/
|
||||
function createProfile(DataTypes.CreateProfileData calldata vars) external;
|
||||
|
||||
/**
|
||||
* @notice Sets a profile's follow module, must be called by the profile owner.
|
||||
*
|
||||
* @param profileId The token ID of the profile to set the follow module for.
|
||||
* @param followModule The follow module to set for the given profile, must be whitelisted.
|
||||
* @param followModuleData The data to be passed to the follow module for initialization.
|
||||
*/
|
||||
function setFollowModule(
|
||||
uint256 profileId,
|
||||
address followModule,
|
||||
bytes calldata followModuleData
|
||||
) external;
|
||||
|
||||
/**
|
||||
* @notice Sets a profile's follow module via signature with the specified parameters.
|
||||
*
|
||||
* @param vars A SetFollowModuleWithSigData struct, including the regular parameters and an EIP712Signature struct.
|
||||
*/
|
||||
function setFollowModuleWithSig(DataTypes.SetFollowModuleWithSigData calldata vars) external;
|
||||
|
||||
/**
|
||||
* @notice Sets a profile's dispatcher, giving that dispatcher rights to publish to that profile.
|
||||
*
|
||||
* @param profileId The token ID of the profile of the profile to set the dispatcher for.
|
||||
* @param dispatcher The dispatcher address to set for the given profile ID.
|
||||
*/
|
||||
function setDispatcher(uint256 profileId, address dispatcher) external;
|
||||
|
||||
/**
|
||||
* @notice Sets a profile's dispatcher via signature with the specified parameters.
|
||||
*
|
||||
* @param vars A SetDispatcherWithSigData struct, including the regular parameters and an EIP712Signature struct.
|
||||
*/
|
||||
function setDispatcherWithSig(DataTypes.SetDispatcherWithSigData calldata vars) external;
|
||||
|
||||
/**
|
||||
* @notice Sets a profile's URI, which is reflected in the `tokenURI()` function.
|
||||
*
|
||||
* @param profileId The token ID of the profile of the profile to set the URI for.
|
||||
* @param imageURI The URI to set for the given profile.
|
||||
*/
|
||||
function setProfileImageURI(uint256 profileId, string calldata imageURI) external;
|
||||
|
||||
/**
|
||||
* @notice Sets a profile's URI via signature with the specified parameters.
|
||||
*
|
||||
* @param vars A SetProfileImageURIWithSigData struct, including the regular parameters and an EIP712Signature struct.
|
||||
*/
|
||||
function setProfileImageURIWithSig(DataTypes.SetProfileImageURIWithSigData calldata vars)
|
||||
external;
|
||||
|
||||
/**
|
||||
* @notice Sets a followNFT URI for a given profile's follow NFT.
|
||||
*
|
||||
* @param profileId The token ID of the profile for which to set the followNFT URI.
|
||||
* @param followNFTURI The follow NFT URI to set.
|
||||
*/
|
||||
function setFollowNFTURI(uint256 profileId, string calldata followNFTURI) external;
|
||||
|
||||
/**
|
||||
* @notice Sets a followNFT URI via signature with the specified parameters.
|
||||
*
|
||||
* @param vars A SetFollowNFTURIWithSigData struct, including the regular parameters and an EIP712Signature struct.
|
||||
*/
|
||||
function setFollowNFTURIWithSig(DataTypes.SetFollowNFTURIWithSigData calldata vars) external;
|
||||
|
||||
/**
|
||||
* @notice Publishes a post to a given profile, must be called by the profile owner.
|
||||
*
|
||||
* @param vars A PostData struct containing the needed parameters.
|
||||
*/
|
||||
function post(DataTypes.PostData calldata vars) external;
|
||||
|
||||
/**
|
||||
* @notice Publishes a post to a given profile via signature with the specified parameters.
|
||||
*
|
||||
* @param vars A PostWithSigData struct containing the regular parameters and an EIP712Signature struct.
|
||||
*/
|
||||
function postWithSig(DataTypes.PostWithSigData calldata vars) external;
|
||||
|
||||
/**
|
||||
* @notice Publishes a comment to a given profile, must be called by the profile owner.
|
||||
*
|
||||
* @param vars A CommentData struct containing the needed parameters.
|
||||
*/
|
||||
function comment(DataTypes.CommentData calldata vars) external;
|
||||
|
||||
/**
|
||||
* @notice Publishes a comment to a given profile via signature with the specified parameters.
|
||||
*
|
||||
*@param vars A CommentWithSigData struct containing the regular parameters and an EIP712Signature struct.
|
||||
*
|
||||
*/
|
||||
function commentWithSig(DataTypes.CommentWithSigData calldata vars) external;
|
||||
|
||||
/**
|
||||
* @notice Publishes a mirror to a given profile, must be called by the profile owner.
|
||||
*
|
||||
* @param vars A MirrorData struct containing the necessary parameters.
|
||||
*/
|
||||
function mirror(DataTypes.MirrorData calldata vars) external;
|
||||
|
||||
/**
|
||||
* @notice Publishes a mirror to a given profile via signature with the specified parameters.
|
||||
*
|
||||
* @param vars A MirrorWithSigData struct containing the regular parameters and an EIP712Signature struct.
|
||||
*/
|
||||
function mirrorWithSig(DataTypes.MirrorWithSigData calldata vars) external;
|
||||
|
||||
/**
|
||||
* @notice Follows the given profiles, executing each profile's follow module logic (if any) and minting followNFTs to the caller.
|
||||
*
|
||||
* NOTE: Both the `profileIds` and `datas` arrays must be of the same length, regardless if the profiles do not have a follow module set.
|
||||
*
|
||||
* @param profileIds The token ID array of the profiles to follow.
|
||||
* @param datas The arbitrary data array to pass to the follow module for each profile if needed.
|
||||
*/
|
||||
function follow(uint256[] calldata profileIds, bytes[] calldata datas) external;
|
||||
|
||||
/**
|
||||
* @notice Follows a given profile via signature with the specified parameters.
|
||||
*
|
||||
* @param vars A FollowWithSigData struct containing the regular parameters as well as the signing follower's address
|
||||
* and an EIP712Signature struct.
|
||||
*/
|
||||
function followWithSig(DataTypes.FollowWithSigData calldata vars) external;
|
||||
|
||||
/**
|
||||
* @notice Collects a given publication, executing collect module logic and minting a collectNFT to the caller.
|
||||
*
|
||||
* @param profileId The token ID of the profile that published the publication to collect.
|
||||
* @param pubId The publication to collect's publication ID.
|
||||
* @param data The arbitrary data to pass to the collect module if needed.
|
||||
*/
|
||||
function collect(
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) external;
|
||||
|
||||
/**
|
||||
* @notice Collects a given publication via signature with the specified parameters.
|
||||
*
|
||||
* @param vars A CollectWithSigData struct containing the regular parameters as well as the collector's address and
|
||||
* an EIP712Signature struct.
|
||||
*/
|
||||
function collectWithSig(DataTypes.CollectWithSigData calldata vars) external;
|
||||
|
||||
/**
|
||||
* @dev Helper function to emit a detailed followNFT transfer event from the hub, to be consumed by frontends to track
|
||||
* followNFT transfers.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
function emitFollowNFTTransferEvent(
|
||||
uint256 profileId,
|
||||
uint256 followNFTId,
|
||||
address from,
|
||||
address to
|
||||
) external;
|
||||
|
||||
/**
|
||||
* @dev Helper function to emit a detailed collectNFT transfer event from the hub, to be consumed by frontends to track
|
||||
* collectNFT transfers.
|
||||
*
|
||||
* @param profileId The token ID of the profile associated with the collect NFT being transferred.
|
||||
* @param pubId The publication ID associated with the collect NFT being transferred.
|
||||
* @param collectNFTId The collectNFT being transferred's token ID.
|
||||
* @param from The address the collectNFT is being transferred from.
|
||||
* @param to The address the collectNFT is being transferred to.
|
||||
*/
|
||||
function emitCollectNFTTransferEvent(
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
uint256 collectNFTId,
|
||||
address from,
|
||||
address to
|
||||
) external;
|
||||
|
||||
/// ************************
|
||||
/// *****VIEW FUNCTIONS*****
|
||||
/// ************************
|
||||
|
||||
/**
|
||||
* @notice Returns whether or not a profile creator is whitelisted.
|
||||
*
|
||||
* @param profileCreator The address of the profile creator to check.
|
||||
*
|
||||
* @return A boolean, true if the profile creator is whitelisted.
|
||||
*/
|
||||
function isProfileCreatorWhitelisted(address profileCreator) external view returns (bool);
|
||||
|
||||
/**
|
||||
* @notice Returns whether or not a follow module is whitelisted.
|
||||
*
|
||||
* @param followModule The address of the follow module to check.
|
||||
*
|
||||
* @return A boolean, true if the the follow module is whitelisted.
|
||||
*/
|
||||
function isFollowModuleWhitelisted(address followModule) external view returns (bool);
|
||||
|
||||
/**
|
||||
* @notice Returns whether or not a reference module is whitelisted.
|
||||
*
|
||||
* @param referenceModule The address of the reference module to check.
|
||||
*
|
||||
* @return A boolean, true if the the reference module is whitelisted.
|
||||
*/
|
||||
function isReferenceModuleWhitelisted(address referenceModule) external view returns (bool);
|
||||
|
||||
/**
|
||||
* @notice Returns whether or not a collect module is whitelisted.
|
||||
*
|
||||
* @param collectModule The address of the collect module to check.
|
||||
*
|
||||
* @return A boolean, true if the the collect module is whitelisted.
|
||||
*/
|
||||
function isCollectModuleWhitelisted(address collectModule) external view returns (bool);
|
||||
|
||||
/**
|
||||
* @notice Returns the currently configured governance address.
|
||||
*
|
||||
* @return The address of the currently configured governance.
|
||||
*/
|
||||
function getGovernance() external view returns (address);
|
||||
|
||||
/**
|
||||
* @notice Returns the dispatcher associated with a profile.
|
||||
*
|
||||
* @param profileId The token ID of the profile to query the dispatcher for.
|
||||
*
|
||||
* @return The dispatcher address associated with the profile.
|
||||
*/
|
||||
function getDispatcher(uint256 profileId) external view returns (address);
|
||||
|
||||
/**
|
||||
* @notice Returns the publication count for a given profile.
|
||||
*
|
||||
* @param profileId The token ID of the profile to query.
|
||||
*
|
||||
* @return The number of publications associated with the queried profile.
|
||||
*/
|
||||
function getPubCount(uint256 profileId) external view returns (uint256);
|
||||
|
||||
/**
|
||||
* @notice Returns the followNFT associated with a given profile, if any.
|
||||
*
|
||||
* @param profileId The token ID of the profile to query the followNFT for.
|
||||
*
|
||||
* @return The followNFT associated with the given profile.
|
||||
*/
|
||||
function getFollowNFT(uint256 profileId) external view returns (address);
|
||||
|
||||
/**
|
||||
* @notice Returns the followNFT URI associated with a given profile.
|
||||
*
|
||||
* @param profileId The token ID of the profile to query the followNFT URI for.
|
||||
*
|
||||
* @return The followNFT URI associated with the given profile.
|
||||
*/
|
||||
function getFollowNFTURI(uint256 profileId) external view returns (string memory);
|
||||
|
||||
/**
|
||||
* @notice Returns the collectNFT associated with a given publication, if any.
|
||||
*
|
||||
* @param profileId The token ID of the profile that published the publication to query.
|
||||
* @param pubId The publication ID of the publication to query.
|
||||
*
|
||||
* @return The address of the collectNFT associated with the queried publication.
|
||||
*/
|
||||
function getCollectNFT(uint256 profileId, uint256 pubId) external view returns (address);
|
||||
|
||||
/**
|
||||
* @notice Returns the follow module associated witha given profile, if any.
|
||||
*
|
||||
* @param profileId The token ID of the profile to query the follow module for.
|
||||
*
|
||||
* @return The address of the follow module associated with the given profile.
|
||||
*/
|
||||
function getFollowModule(uint256 profileId) external view returns (address);
|
||||
|
||||
/**
|
||||
* @notice Returns the collect module associated with a given publication.
|
||||
*
|
||||
* @param profileId The token ID of the profile that published the publication to query.
|
||||
* @param pubId The publication ID of the publication to query.
|
||||
*
|
||||
* @return The address of the collect module associated with the queried publication.
|
||||
*/
|
||||
function getCollectModule(uint256 profileId, uint256 pubId) external view returns (address);
|
||||
|
||||
/**
|
||||
* @notice Returns the reference module associated witha given profile, if any.
|
||||
*
|
||||
* @param profileId The token ID of the profile that published the publication to querythe reference module for.
|
||||
* @param pubId The publication ID of the publication to query the reference module for.
|
||||
*
|
||||
* @return The address of the reference module associated with the given profile.
|
||||
*/
|
||||
function getReferenceModule(uint256 profileId, uint256 pubId) external view returns (address);
|
||||
|
||||
/**
|
||||
* @notice Returns the handle associated with a profile.
|
||||
*
|
||||
* @param profileId The token ID of the profile to query the handle for.
|
||||
*
|
||||
* @return The handle associated with the profile.
|
||||
*/
|
||||
function getHandle(uint256 profileId) external view returns (string memory);
|
||||
|
||||
/**
|
||||
* @notice Returns the publication pointer (profileId & pubId) associated with a given publication.
|
||||
*
|
||||
* @param profileId The token ID of the profile that published the publication to query the pointer for.
|
||||
* @param pubId The publication ID of the publication to query the pointer for.
|
||||
*
|
||||
* @return
|
||||
* First, the profile ID of the profile the current publication is pointing to.
|
||||
* Second, the publication ID of the publication the current publication is pointing to.
|
||||
*/
|
||||
function getPubPointer(uint256 profileId, uint256 pubId)
|
||||
external
|
||||
view
|
||||
returns (uint256, uint256);
|
||||
|
||||
/**
|
||||
* @notice Returns the URI associated with a given publication.
|
||||
*
|
||||
* @param profileId The token ID of the profile that published the publication to query.
|
||||
* @param pubId The publication ID of the publication to query.
|
||||
*
|
||||
* @return The URI associated with a given publication.
|
||||
*/
|
||||
function getContentURI(uint256 profileId, uint256 pubId) external view returns (string memory);
|
||||
|
||||
/**
|
||||
* @notice Returns the profile token ID according to a given handle.
|
||||
*
|
||||
* @param handle The handle to resolve the profile token ID with.
|
||||
*
|
||||
* @return The profile ID the passed handle points to.
|
||||
*/
|
||||
function getProfileIdByHandle(string calldata handle) external view returns (uint256);
|
||||
|
||||
/**
|
||||
* @notice Returns the full profile struct associated with a given profile token ID.
|
||||
*
|
||||
* @param profileId The token ID of the profile to query.
|
||||
*/
|
||||
function getProfile(uint256 profileId) external view returns (DataTypes.ProfileStruct memory);
|
||||
|
||||
/**
|
||||
* @notice Returns the full publication struct for a given publication.
|
||||
*
|
||||
* @param profileId The token ID of the profile that published the publication to query.
|
||||
* @param pubId The publication ID of the publication to query.
|
||||
*
|
||||
* @return The PublicationStruct associated with the queried publication.
|
||||
*/
|
||||
function getPub(uint256 profileId, uint256 pubId)
|
||||
external
|
||||
view
|
||||
returns (DataTypes.PublicationStruct memory);
|
||||
|
||||
/**
|
||||
* @notice Returns the publication type associated with a given publication.
|
||||
*
|
||||
* @param profileId The token ID of the profile that published the publication to query.
|
||||
* @param pubId The publication ID of the publication to query.
|
||||
*
|
||||
* @return The publication type, as a member of an enum (either "post," "comment" or "mirror").
|
||||
*/
|
||||
function getPubType(uint256 profileId, uint256 pubId) external view returns (DataTypes.PubType);
|
||||
}
|
||||
70
contracts/interfaces/ILensNFTBase.sol
Normal file
70
contracts/interfaces/ILensNFTBase.sol
Normal file
@@ -0,0 +1,70 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {DataTypes} from '../libraries/DataTypes.sol';
|
||||
|
||||
/**
|
||||
* @title ILensNFTBase
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This is the interface for the LensNFTBase contract, from which all Lens NFTs inherit.
|
||||
* It is an expansion of a very slightly modified ERC721Enumerable contract, which allows expanded
|
||||
* meta-transaction functionality.
|
||||
*/
|
||||
interface ILensNFTBase {
|
||||
/**
|
||||
* @notice Implementation of an EIP-712 permit function for an ERC-721 NFT. We don't need to check
|
||||
* if the tokenId exists, since the function calls ownerOf(tokenId), which reverts if the tokenId does
|
||||
* not exist.
|
||||
*
|
||||
* @param spender The NFT spender.
|
||||
* @param tokenId The NFT token ID to approve.
|
||||
* @param sig The EIP712 signature struct.
|
||||
*/
|
||||
function permit(
|
||||
address spender,
|
||||
uint256 tokenId,
|
||||
DataTypes.EIP712Signature calldata sig
|
||||
) external;
|
||||
|
||||
/**
|
||||
* @notice Implementation of an EIP-712 permit-style function for ERC-721 operator approvals. Allows
|
||||
* an operator address to control all NFTs a given owner owns.
|
||||
*
|
||||
* @param owner The owner to set operator approvals for.
|
||||
* @param operator The operator to approve.
|
||||
* @param approved Whether to approve or revoke approval from the operator.
|
||||
* @param sig The EIP712 signature struct.
|
||||
*/
|
||||
function permitForAll(
|
||||
address owner,
|
||||
address operator,
|
||||
bool approved,
|
||||
DataTypes.EIP712Signature calldata sig
|
||||
) external;
|
||||
|
||||
/**
|
||||
* @notice Burns an NFT, removing it from circulation and essentially destroying it. This function can only
|
||||
* be called by the NFT to burn's owner.
|
||||
*
|
||||
* @param tokenId The token ID of the token to burn.
|
||||
*/
|
||||
function burn(uint256 tokenId) external;
|
||||
|
||||
/**
|
||||
* @notice Implementation of an EIP-712 permit-style function for token burning. Allows anyone to burn
|
||||
* a token on behalf of the owner with a signature.
|
||||
*
|
||||
* @param tokenId The token ID of the token to burn.
|
||||
* @param sig The EIP712 signature struct.
|
||||
*/
|
||||
function burnWithSig(uint256 tokenId, DataTypes.EIP712Signature calldata sig) external;
|
||||
|
||||
/**
|
||||
* @notice Returns the domain separator for this NFT contract.
|
||||
*
|
||||
* @return The domain separator.
|
||||
*/
|
||||
function getDomainSeparator() external view returns (bytes32);
|
||||
}
|
||||
82
contracts/interfaces/IModuleGlobals.sol
Normal file
82
contracts/interfaces/IModuleGlobals.sol
Normal file
@@ -0,0 +1,82 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
/**
|
||||
* @title IModuleGlobals
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This is the interface for the ModuleGlobals contract, a data providing contract to be queried by modules
|
||||
* for the most up-to-date parameters.
|
||||
*/
|
||||
interface IModuleGlobals {
|
||||
/**
|
||||
* @notice Sets the governance address. This function can only be called by governance.
|
||||
*
|
||||
* @param newGovernance The new governance address to set.
|
||||
*/
|
||||
function setGovernance(address newGovernance) external;
|
||||
|
||||
/**
|
||||
* @notice Sets the treasury address. This function can only be called by governance.
|
||||
*
|
||||
* @param newTreasury The new treasury address to set.
|
||||
*/
|
||||
function setTreasury(address newTreasury) external;
|
||||
|
||||
/**
|
||||
* @notice Sets the treasury fee. This function can only be called by governance.
|
||||
*
|
||||
* @param newTreasuryFee The new treasury fee to set.
|
||||
*/
|
||||
function setTreasuryFee(uint16 newTreasuryFee) external;
|
||||
|
||||
/**
|
||||
* @notice Adds or removes a currency from the whitelist. This function can only be called by governance.
|
||||
*
|
||||
* @param currency The currency to add or remove from the whitelist.
|
||||
* @param toWhitelist Whether to add or remove the currency from the whitelist.
|
||||
*/
|
||||
function whitelistCurrency(address currency, bool toWhitelist) external;
|
||||
|
||||
/// ************************
|
||||
/// *****VIEW FUNCTIONS*****
|
||||
/// ************************
|
||||
|
||||
/**
|
||||
* @notice Returns whether a currency is whitelisted.
|
||||
*
|
||||
* @param currency The currency to query the whitelist for.
|
||||
*
|
||||
* @return Whether the queried currency is whitelisted.
|
||||
*/
|
||||
function isCurrencyWhitelisted(address currency) external view returns (bool);
|
||||
|
||||
/**
|
||||
* @notice Returns the governance address.
|
||||
*
|
||||
* @return The governance address.
|
||||
*/
|
||||
function getGovernance() external view returns (address);
|
||||
|
||||
/**
|
||||
* @notice Returns the treasury address.
|
||||
*
|
||||
* @return The treasury address.
|
||||
*/
|
||||
function getTreasury() external view returns (address);
|
||||
|
||||
/**
|
||||
* @notice Returns the treasury fee.
|
||||
*
|
||||
* @return The treasury fee.
|
||||
*/
|
||||
function getTreasuryFee() external view returns (uint16);
|
||||
|
||||
/**
|
||||
* @notice Returns the treasury address and treasury fee in a single call.
|
||||
*
|
||||
* @return The treasury address and the treasury fee.
|
||||
*/
|
||||
function getTreasuryData() external view returns (address, uint16);
|
||||
}
|
||||
52
contracts/interfaces/IReferenceModule.sol
Normal file
52
contracts/interfaces/IReferenceModule.sol
Normal file
@@ -0,0 +1,52 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
/**
|
||||
* @title IReferenceModule
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This is the standard interface for all Lens-compatible ReferenceModules.
|
||||
*/
|
||||
interface IReferenceModule {
|
||||
/**
|
||||
* @notice Initializes data for a given publication being published. This can only be called by the hub.
|
||||
* @param profileId The token ID of the profile publishing the publication.
|
||||
* @param pubId The associated publication's LensHub publication ID.
|
||||
* @param data Arbitrary data passed from the user to be decoded.
|
||||
*
|
||||
* @return An abi encoded byte array encapsulating the execution's state changes. This will be emitted by the
|
||||
* hub alongside the collect module's address and should be consumed by front ends.
|
||||
*/
|
||||
function initializeReferenceModule(
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) external returns (bytes memory);
|
||||
|
||||
/**
|
||||
* @notice Processes a comment action referencing a given publication. This can only be called by the hub.
|
||||
*
|
||||
* @param profileId The token ID of the profile associated with the publication being published.
|
||||
* @param profileIdPointed The profile ID of the profile associated the publication being referenced.
|
||||
* @param pubIdPointed The publication ID of the publication being referenced.
|
||||
*/
|
||||
function processComment(
|
||||
uint256 profileId,
|
||||
uint256 profileIdPointed,
|
||||
uint256 pubIdPointed
|
||||
) external;
|
||||
|
||||
/**
|
||||
* @notice Processes a mirror action referencing a given publication. This can only be called by the hub.
|
||||
*
|
||||
* @param profileId The token ID of the profile associated with the publication being published.
|
||||
* @param profileIdPointed The profile ID of the profile associated the publication being referenced.
|
||||
* @param pubIdPointed The publication ID of the publication being referenced.
|
||||
*/
|
||||
function processMirror(
|
||||
uint256 profileId,
|
||||
uint256 profileIdPointed,
|
||||
uint256 pubIdPointed
|
||||
) external;
|
||||
}
|
||||
11
contracts/libraries/Constants.sol
Normal file
11
contracts/libraries/Constants.sol
Normal file
@@ -0,0 +1,11 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
library Constants {
|
||||
string internal constant FOLLOW_NFT_NAME_SUFFIX = '-Follower';
|
||||
string internal constant FOLLOW_NFT_SYMBOL_SUFFIX = '-Fl';
|
||||
string internal constant COLLECT_NFT_NAME_INFIX = '-Collect-';
|
||||
string internal constant COLLECT_NFT_SYMBOL_INFIX = '-Cl-';
|
||||
uint8 internal constant MAX_HANDLE_LENGTH = 31;
|
||||
}
|
||||
330
contracts/libraries/DataTypes.sol
Normal file
330
contracts/libraries/DataTypes.sol
Normal file
@@ -0,0 +1,330 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
/**
|
||||
* @title DataTypes
|
||||
* @author Lens
|
||||
*
|
||||
* @notice A standard library of data types used throughout the Lens protocol.
|
||||
*/
|
||||
library DataTypes {
|
||||
/**
|
||||
* @notice An enum containing the different states the protocol can be in, limiting certain actions.
|
||||
*
|
||||
* @param Unpaused The fully unpaused state.
|
||||
* @param PublishingPaused The state where only publication creation functions are paused.
|
||||
* @param Paused The fully paused state.
|
||||
*/
|
||||
enum ProtocolState {
|
||||
Unpaused,
|
||||
PublishingPaused,
|
||||
Paused
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice An enum specifically used in a helper function to easily retrieve the publication type for integrations.
|
||||
*
|
||||
* @param Post A standard post, having a URI, a collect module but no pointer to another publication.
|
||||
* @param Comment A comment, having a URI, a collect module and a pointer to another publication.
|
||||
* @param Mirror A mirror, having a pointer to another publication, but no URI or collect module.
|
||||
* @param Nonexistent An indicator showing the queried publication does not exist.
|
||||
*/
|
||||
enum PubType {
|
||||
Post,
|
||||
Comment,
|
||||
Mirror,
|
||||
Nonexistent
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice A struct containing the necessary information to reconstruct an EIP-712 typed data signature.
|
||||
*
|
||||
* @param v The signature's recovery parameter.
|
||||
* @param r The signature's r parameter.
|
||||
* @param s The signature's s parameter
|
||||
* @param deadline The signature's deadline
|
||||
*/
|
||||
struct EIP712Signature {
|
||||
uint8 v;
|
||||
bytes32 r;
|
||||
bytes32 s;
|
||||
uint256 deadline;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice A struct containing profile data.
|
||||
*
|
||||
* @param pubCount The number of publications made to this profile.
|
||||
* @param followModule The address of the current follow module in use by this profile, can be empty.
|
||||
* @param followNFT The address of the followNFT associated with this profile, can be empty..
|
||||
* @param handle The profile's associated handle.
|
||||
* @param imageURI The URI to be used for the profile's image.
|
||||
* @param followNFTURI The URI to be used for the follow NFT.
|
||||
*/
|
||||
struct ProfileStruct {
|
||||
uint256 pubCount;
|
||||
address followModule;
|
||||
address followNFT;
|
||||
string handle;
|
||||
string imageURI;
|
||||
string followNFTURI;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice A struct containing data associated with each new publication.
|
||||
*
|
||||
* @param profileIdPointed The profile token ID this publication points to, for mirrors and comments.
|
||||
* @param pubIdPointed 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 profileIdPointed;
|
||||
uint256 pubIdPointed;
|
||||
string contentURI;
|
||||
address referenceModule;
|
||||
address collectModule;
|
||||
address collectNFT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice A struct containing the parameters required for the `createProfile()` function.
|
||||
*
|
||||
* @param to The address receiving the profile.
|
||||
* @param handle The handle to set for the profile, must be unique and non-empty.
|
||||
* @param imageURI The URI to set for the profile image.
|
||||
* @param followModule The follow module to use, can be the zero address.
|
||||
* @param followModuleData The follow module initialization data, if any.
|
||||
* @param followNFTURI The URI to use for the follow NFT.
|
||||
*/
|
||||
struct CreateProfileData {
|
||||
address to;
|
||||
string handle;
|
||||
string imageURI;
|
||||
address followModule;
|
||||
bytes followModuleData;
|
||||
string followNFTURI;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice A struct containing the parameters required for the `setFollowModuleWithSig()` function. Parameters are
|
||||
* the same as the regular `setFollowModule()` function, with an added EIP712Signature.
|
||||
*
|
||||
* @param profileId The token ID of the profile to change the followModule for.
|
||||
* @param followModule The followModule to set for the given profile, must be whitelisted.
|
||||
* @param followModuleData The data to be passed to the followModule for initialization.
|
||||
* @param sig The EIP712Signature struct containing the profile owner's signature.
|
||||
*/
|
||||
struct SetFollowModuleWithSigData {
|
||||
uint256 profileId;
|
||||
address followModule;
|
||||
bytes followModuleData;
|
||||
EIP712Signature sig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice A struct containing the parameters required for the `setDispatcherWithSig()` function. Parameters are the same
|
||||
* as the regular `setDispatcher()` function, with an added EIP712Signature.
|
||||
*
|
||||
* @param profileId The token ID of the profile to set the dispatcher for.
|
||||
* @param dispatcher The dispatcher address to set for the profile.
|
||||
* @param sig The EIP712Signature struct containing the profile owner's signature.
|
||||
*/
|
||||
struct SetDispatcherWithSigData {
|
||||
uint256 profileId;
|
||||
address dispatcher;
|
||||
EIP712Signature sig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice A struct containing the parameters required for the `setProfileImageURIWithSig()` function. Parameters are the same
|
||||
* as the regular `setProfileImageURI()` function, with an added EIP712Signature.
|
||||
*
|
||||
* @param profileId The token ID of the profile to set the URI for.
|
||||
* @param imageURI The URI to set for the given profile image.
|
||||
* @param sig The EIP712Signature struct containing the profile owner's signature.
|
||||
*/
|
||||
struct SetProfileImageURIWithSigData {
|
||||
uint256 profileId;
|
||||
string imageURI;
|
||||
EIP712Signature sig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice A struct containing the parameters required for the `setFollowNFTURIWithSig()` function. Parameters are the same
|
||||
* as the regular `setFollowNFTURI()` function, with an added EIP712Signature.
|
||||
*
|
||||
* @param profileId The token ID of the profile for which to set the followNFT URI.
|
||||
* @param followNFTURI The follow NFT URI to set.
|
||||
* @param sig The EIP712Signature struct containing the followNFT's associated profile owner's signature.
|
||||
*/
|
||||
struct SetFollowNFTURIWithSigData {
|
||||
uint256 profileId;
|
||||
string followNFTURI;
|
||||
EIP712Signature sig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice A struct containing the parameters required for the `post()` function.
|
||||
*
|
||||
* @param profileId The token ID of the profile to publish to.
|
||||
* @param contentURI The URI to set for this new publication.
|
||||
* @param collectModule The collect module to set for this new publication.
|
||||
* @param collectModuleData The data to pass to the collect module's initialization.
|
||||
* @param referenceModule The reference module to set for the given publication, must be whitelisted.
|
||||
* @param referenceModuleData The data to be passed to the reference module for initialization.
|
||||
*/
|
||||
struct PostData {
|
||||
uint256 profileId;
|
||||
string contentURI;
|
||||
address collectModule;
|
||||
bytes collectModuleData;
|
||||
address referenceModule;
|
||||
bytes referenceModuleData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice A struct containing the parameters required for the `postWithSig()` function. Parameters are the same as
|
||||
* the regular `post()` function, with an added EIP712Signature.
|
||||
*
|
||||
* @param profileId The token ID of the profile to publish to.
|
||||
* @param contentURI The URI to set for this new publication.
|
||||
* @param collectModule The collectModule to set for this new publication.
|
||||
* @param collectModuleData The data to pass to the collectModule's initialization.
|
||||
* @param referenceModule The reference module to set for the given publication, must be whitelisted.
|
||||
* @param referenceModuleData The data to be passed to the reference module for initialization.
|
||||
* @param sig The EIP712Signature struct containing the profile owner's signature.
|
||||
*/
|
||||
struct PostWithSigData {
|
||||
uint256 profileId;
|
||||
string contentURI;
|
||||
address collectModule;
|
||||
bytes collectModuleData;
|
||||
address referenceModule;
|
||||
bytes referenceModuleData;
|
||||
EIP712Signature sig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice A struct containing the parameters required for the `comment()` function.
|
||||
*
|
||||
* @param profileId The token ID of the profile to publish to.
|
||||
* @param contentURI The URI to set for this new publication.
|
||||
* @param profileIdPointed The profile token ID to point the comment to.
|
||||
* @param pubIdPointed The publication ID to point the comment to.
|
||||
* @param collectModule The collect module to set for this new publication.
|
||||
* @param collectModuleData The data to pass to the collect module's initialization.
|
||||
* @param referenceModule The reference module to set for the given publication, must be whitelisted.
|
||||
* @param referenceModuleData The data to be passed to the reference module for initialization.
|
||||
*/
|
||||
struct CommentData {
|
||||
uint256 profileId;
|
||||
string contentURI;
|
||||
uint256 profileIdPointed;
|
||||
uint256 pubIdPointed;
|
||||
address collectModule;
|
||||
bytes collectModuleData;
|
||||
address referenceModule;
|
||||
bytes referenceModuleData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice A struct containing the parameters required for the `commentWithSig()` function. Parameters are the same as
|
||||
* the regular `comment()` function, with an added EIP712Signature.
|
||||
*
|
||||
* @param profileId The token ID of the profile to publish to.
|
||||
* @param contentURI The URI to set for this new publication.
|
||||
* @param profileIdPointed The profile token ID to point the comment to.
|
||||
* @param pubIdPointed The publication ID to point the comment to.
|
||||
* @param collectModule The collectModule to set for this new publication.
|
||||
* @param collectModuleData The data to pass to the collectModule's initialization.
|
||||
* @param referenceModule The reference module to set for the given publication, must be whitelisted.
|
||||
* @param referenceModuleData The data to be passed to the reference module for initialization.
|
||||
* @param sig The EIP712Signature struct containing the profile owner's signature.
|
||||
*/
|
||||
struct CommentWithSigData {
|
||||
uint256 profileId;
|
||||
string contentURI;
|
||||
uint256 profileIdPointed;
|
||||
uint256 pubIdPointed;
|
||||
address collectModule;
|
||||
bytes collectModuleData;
|
||||
address referenceModule;
|
||||
bytes referenceModuleData;
|
||||
EIP712Signature sig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice A struct containing the parameters required for the `mirror()` function.
|
||||
*
|
||||
* @param profileId The token ID of the profile to publish to.
|
||||
* @param profileIdPointed The profile token ID to point the mirror to.
|
||||
* @param pubIdPointed The publication ID to point the mirror to.
|
||||
* @param referenceModule The reference module to set for the given publication, must be whitelisted.
|
||||
* @param referenceModuleData The data to be passed to the reference module for initialization.
|
||||
*/
|
||||
struct MirrorData {
|
||||
uint256 profileId;
|
||||
uint256 profileIdPointed;
|
||||
uint256 pubIdPointed;
|
||||
address referenceModule;
|
||||
bytes referenceModuleData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice A struct containing the parameters required for the `mirrorWithSig()` function. Parameters are the same as
|
||||
* the regular `mirror()` function, with an added EIP712Signature.
|
||||
*
|
||||
* @param profileId The token ID of the profile to publish to.
|
||||
* @param profileIdPointed The profile token ID to point the mirror to.
|
||||
* @param pubIdPointed The publication ID to point the mirror to.
|
||||
* @param referenceModule The reference module to set for the given publication, must be whitelisted.
|
||||
* @param referenceModuleData The data to be passed to the reference module for initialization.
|
||||
* @param sig The EIP712Signature struct containing the profile owner's signature.
|
||||
*/
|
||||
struct MirrorWithSigData {
|
||||
uint256 profileId;
|
||||
uint256 profileIdPointed;
|
||||
uint256 pubIdPointed;
|
||||
address referenceModule;
|
||||
bytes referenceModuleData;
|
||||
EIP712Signature sig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice A struct containing the parameters required for the `followWithSig()` function. Parameters are the same
|
||||
* as the regular `follow()` function, with the follower's (signer) address and an EIP712Signature added.
|
||||
*
|
||||
* @param follower The follower which is the message signer.
|
||||
* @param profileIds The array of token IDs of the profiles to follow.
|
||||
* @param datas The array of arbitrary data to pass to the followModules if needed.
|
||||
* @param sig The EIP712Signature struct containing the follower's signature.
|
||||
*/
|
||||
struct FollowWithSigData {
|
||||
address follower;
|
||||
uint256[] profileIds;
|
||||
bytes[] datas;
|
||||
EIP712Signature sig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice A struct containing the parameters required for the `collectWithSig()` function. Parameters are the same as
|
||||
* the regular `collect()` function, with the collector's (signer) address and an EIP712Signature added.
|
||||
*
|
||||
* @param collector The collector which is the message signer.
|
||||
* @param profileId The token ID of the profile that published the publication to collect.
|
||||
* @param pubId The publication to collect's publication ID.
|
||||
* @param data The arbitrary data to pass to the collectModule if needed.
|
||||
* @param sig The EIP712Signature struct containing the collector's signature.
|
||||
*/
|
||||
struct CollectWithSigData {
|
||||
address collector;
|
||||
uint256 profileId;
|
||||
uint256 pubId;
|
||||
bytes data;
|
||||
EIP712Signature sig;
|
||||
}
|
||||
}
|
||||
45
contracts/libraries/Errors.sol
Normal file
45
contracts/libraries/Errors.sol
Normal file
@@ -0,0 +1,45 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
library Errors {
|
||||
error CannotInitImplementation();
|
||||
error Initialized();
|
||||
error SignatureExpired();
|
||||
error ZeroSpender();
|
||||
error SignatureInvalid();
|
||||
error NotOwnerOrApproved();
|
||||
error NotHub();
|
||||
error TokenDoesNotExist();
|
||||
error NotGovernance();
|
||||
error NotGovernanceOrEmergencyAdmin();
|
||||
error CallerNotWhitelistedModule();
|
||||
error CollectModuleNotWhitelisted();
|
||||
error FollowModuleNotWhitelisted();
|
||||
error ReferenceModuleNotWhitelisted();
|
||||
error ProfileCreatorNotWhitelisted();
|
||||
error NotProfileOwner();
|
||||
error NotProfileOwnerOrDispatcher();
|
||||
error PublicationDoesNotExist();
|
||||
error HandleTaken();
|
||||
error HandleLengthInvalid();
|
||||
error HandleContainsInvalidCharacters();
|
||||
error CallerNotFollowNFT();
|
||||
error CallerNotCollectNFT();
|
||||
error BlockNumberInvalid();
|
||||
error ArrayMismatch();
|
||||
|
||||
// Module Errors
|
||||
error InitParamsInvalid();
|
||||
error ZeroCurrency();
|
||||
error CollectExpired();
|
||||
error FollowInvalid();
|
||||
error ModuleDataMismatch();
|
||||
error FollowNotApproved();
|
||||
error MintLimitExceeded();
|
||||
error CollectNotAllowed();
|
||||
|
||||
// MultiState Errors
|
||||
error Paused();
|
||||
error PublishingPaused();
|
||||
}
|
||||
469
contracts/libraries/Events.sol
Normal file
469
contracts/libraries/Events.sol
Normal file
@@ -0,0 +1,469 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {DataTypes} from './DataTypes.sol';
|
||||
|
||||
library Events {
|
||||
/**
|
||||
* @dev Emitted when the NFT contract's name and symbol are set at initialization.
|
||||
*
|
||||
* @param name The NFT name set.
|
||||
* @param symbol The NFT symbol set.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event BaseInitialized(string name, string symbol, uint256 timestamp);
|
||||
|
||||
/**
|
||||
* @dev Emitted when the hub state is set.
|
||||
*
|
||||
* @param caller The caller who set the state.
|
||||
* @param prevState The previous protocol state, an enum of either `Paused`, `PublishingPaused` or `Unpaused`.
|
||||
* @param newState The newly set state, an enum of either `Paused`, `PublishingPaused` or `Unpaused`.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event StateSet(
|
||||
address indexed caller,
|
||||
DataTypes.ProtocolState indexed prevState,
|
||||
DataTypes.ProtocolState indexed newState,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
/**
|
||||
* @dev Emitted when the governance address is changed. We emit the caller even though it should be the previous
|
||||
* governance address, as we cannot guarantee this will always be the case due to upgradeability.
|
||||
*
|
||||
* @param caller The caller who set the governance address.
|
||||
* @param prevGovernance The previous governance address.
|
||||
* @param newGovernance The new governance address set.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event GovernanceSet(
|
||||
address indexed caller,
|
||||
address indexed prevGovernance,
|
||||
address indexed newGovernance,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
/**
|
||||
* @dev Emitted when the emergency admin is changed. We emit the caller even though it should be the previous
|
||||
* governance address, as we cannot guarantee this will always be the case due to upgradeability.
|
||||
*
|
||||
* @param caller The caller who set the emergency admin address.
|
||||
* @param oldEmergencyAdmin The previous emergency admin address.
|
||||
* @param newEmergencyAdmin The new emergency admin address set.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event EmergencyAdminSet(
|
||||
address indexed caller,
|
||||
address indexed oldEmergencyAdmin,
|
||||
address indexed newEmergencyAdmin,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
/**
|
||||
* @dev Emitted when a profile creator is added to or removed from the whitelist.
|
||||
*
|
||||
* @param profileCreator The address of the profile creator.
|
||||
* @param whitelisted Whether or not the profile creator is being added to the whitelist.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event ProfileCreatorWhitelisted(
|
||||
address indexed profileCreator,
|
||||
bool indexed whitelisted,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
/**
|
||||
* @dev Emitted when a follow module is added to or removed from the whitelist.
|
||||
*
|
||||
* @param followModule The address of the follow module.
|
||||
* @param whitelisted Whether or not the follow module is being added to the whitelist.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event FollowModuleWhitelisted(
|
||||
address indexed followModule,
|
||||
bool indexed whitelisted,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
/**
|
||||
* @dev Emitted when a reference module is added to or removed from the whitelist.
|
||||
*
|
||||
* @param referenceModule The address of the reference module.
|
||||
* @param whitelisted Whether or not the reference module is being added to the whitelist.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event ReferenceModuleWhitelisted(
|
||||
address indexed referenceModule,
|
||||
bool indexed whitelisted,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
/**
|
||||
* @dev Emitted when a collect module is added to or removed from the whitelist.
|
||||
*
|
||||
* @param collectModule The address of the collect module.
|
||||
* @param whitelisted Whether or not the collect module is being added to the whitelist.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event CollectModuleWhitelisted(
|
||||
address indexed collectModule,
|
||||
bool indexed whitelisted,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
/**
|
||||
* @dev Emitted when a profile is created.
|
||||
*
|
||||
* @param profileId The newly created profile's token ID.
|
||||
* @param creator The profile creator, who created the token with the given profile ID.
|
||||
* @param to The address receiving the profile with the given profile ID.
|
||||
* @param handle The handle set for the profile.
|
||||
* @param imageURI The image uri set for the profile.
|
||||
* @param followModule The profile's newly set follow module. This CAN be the zero address.
|
||||
* @param followModuleReturnData The data returned from the follow module's initialization. This is abi encoded
|
||||
* and totally depends on the follow module chosen.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event ProfileCreated(
|
||||
uint256 indexed profileId,
|
||||
address indexed creator,
|
||||
address indexed to,
|
||||
string handle,
|
||||
string imageURI,
|
||||
address followModule,
|
||||
bytes followModuleReturnData,
|
||||
string followNFTURI,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
/**
|
||||
* @dev Emitted when a dispatcher is set for a specific profile.
|
||||
*
|
||||
* @param profileId The token ID of the profile for which the dispatcher is set.
|
||||
* @param dispatcher The dispatcher set for the given profile.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event DispatcherSet(uint256 indexed profileId, address indexed dispatcher, uint256 timestamp);
|
||||
|
||||
/**
|
||||
* @dev Emitted when a profile's URI is set.
|
||||
*
|
||||
* @param profileId The token ID of the profile for which the URI is set.
|
||||
* @param imageURI The URI set for the given profile.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event ProfileImageURISet(uint256 indexed profileId, string imageURI, uint256 timestamp);
|
||||
|
||||
/**
|
||||
* @dev Emitted when a follow NFT's URI is set.
|
||||
*
|
||||
* @param profileId The token ID of the profile for which the followNFT URI is set.
|
||||
* @param followNFTURI The follow NFT URI set.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event FollowNFTURISet(uint256 indexed profileId, string followNFTURI, uint256 timestamp);
|
||||
|
||||
/**
|
||||
* @dev Emitted when a profile's follow module is set.
|
||||
*
|
||||
* @param profileId The profile's token ID.
|
||||
* @param followModule The profile's newly set follow module. This CAN be the zero address.
|
||||
* @param followModuleReturnData The data returned from the follow module's initialization. This is abi encoded
|
||||
* and totally depends on the follow module chosen.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event FollowModuleSet(
|
||||
uint256 indexed profileId,
|
||||
address followModule,
|
||||
bytes followModuleReturnData,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
/**
|
||||
* @dev Emitted when a "post" is published.
|
||||
*
|
||||
* @param profileId The profile's token ID.
|
||||
* @param pubId The new publication's ID.
|
||||
* @param contentURI The URI mapped to this new publication.
|
||||
* @param collectModule The collect module mapped to this new publication. This CANNOT be the zero address.
|
||||
* @param collectModuleReturnData The data returned from the collect module's initialization for this given
|
||||
* publication. This is abi encoded and totally depends on the collect module chosen.
|
||||
* @param referenceModule The reference module set for this publication.
|
||||
* @param referenceModuleReturnData The data returned from the reference module at initialization. This is abi
|
||||
* encoded and totally depends on the reference module chosen.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event PostCreated(
|
||||
uint256 indexed profileId,
|
||||
uint256 indexed pubId,
|
||||
string contentURI,
|
||||
address collectModule,
|
||||
bytes collectModuleReturnData,
|
||||
address referenceModule,
|
||||
bytes referenceModuleReturnData,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
/**
|
||||
* @dev Emitted when a "comment" is published.
|
||||
*
|
||||
* @param profileId The profile's token ID.
|
||||
* @param pubId The new publication's ID.
|
||||
* @param contentURI The URI mapped to this new publication.
|
||||
* @param profileIdPointed The profile token ID that this comment points to.
|
||||
* @param pubIdPointed The publication ID that this comment points to.
|
||||
* @param collectModule The collect module mapped to this new publication. This CANNOT be the zero address.
|
||||
* @param collectModuleReturnData The data returned from the collect module's initialization for this given
|
||||
* publication. This is abi encoded and totally depends on the collect module chosen.
|
||||
* @param referenceModule The reference module set for this publication.
|
||||
* @param referenceModuleReturnData The data returned from the reference module at initialization. This is abi
|
||||
* encoded and totally depends on the reference module chosen.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event CommentCreated(
|
||||
uint256 indexed profileId,
|
||||
uint256 indexed pubId,
|
||||
string contentURI,
|
||||
uint256 profileIdPointed,
|
||||
uint256 pubIdPointed,
|
||||
address collectModule,
|
||||
bytes collectModuleReturnData,
|
||||
address referenceModule,
|
||||
bytes referenceModuleReturnData,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
/**
|
||||
* @dev Emitted when a "mirror" is published.
|
||||
*
|
||||
* @param profileId The profile's token ID.
|
||||
* @param pubId The new publication's ID.
|
||||
* @param profileIdPointed The profile token ID that this mirror points to.
|
||||
* @param pubIdPointed The publication ID that this mirror points to.
|
||||
* @param referenceModule The reference module set for this publication.
|
||||
* @param referenceModuleReturnData The data returned from the reference module at initialization. This is abi
|
||||
* encoded and totally depends on the reference module chosen.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event MirrorCreated(
|
||||
uint256 indexed profileId,
|
||||
uint256 indexed pubId,
|
||||
uint256 profileIdPointed,
|
||||
uint256 pubIdPointed,
|
||||
address referenceModule,
|
||||
bytes referenceModuleReturnData,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
/**
|
||||
* @dev Emitted when a followNFT clone is deployed using a lazy deployment pattern.
|
||||
*
|
||||
* @param profileId The token ID of the profile to which this followNFT is associated.
|
||||
* @param followNFT The address of the newly deployed followNFT clone.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event FollowNFTDeployed(
|
||||
uint256 indexed profileId,
|
||||
address indexed followNFT,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
/**
|
||||
* @dev Emitted upon a successful follow action.
|
||||
*
|
||||
* @param follower The address following the profile.
|
||||
* @param profileIds The profile token ID array of the profiles being followed.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event Followed(address indexed follower, uint256[] profileIds, uint256 timestamp);
|
||||
|
||||
/**
|
||||
* @dev Emitted when a collectNFT clone is deployed using a lazy deployment pattern.
|
||||
*
|
||||
* @param profileId The publisher's profile token ID.
|
||||
* @param pubId The publication associated with the newly deployed collectNFT clone's ID.
|
||||
* @param collectNFT The address of the newly deployed collectNFT clone.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event CollectNFTDeployed(
|
||||
uint256 indexed profileId,
|
||||
uint256 indexed pubId,
|
||||
address indexed collectNFT,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
/**
|
||||
* @dev Emitted upon a successful collect action.
|
||||
*
|
||||
* @param collector The address collecting the publication.
|
||||
* @param profileId The token ID of the profile that the collect was initiated towards, useful to differentiate mirrors.
|
||||
* @param pubId The publication ID that the collect was initiated towards, useful to differentiate mirrors.
|
||||
* @param rootProfileId The profile token ID of the profile whose publication is being collected.
|
||||
* @param rootPubId The publication ID of the publication being collected.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event Collected(
|
||||
address indexed collector,
|
||||
uint256 indexed profileId,
|
||||
uint256 indexed pubId,
|
||||
uint256 rootProfileId,
|
||||
uint256 rootPubId,
|
||||
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.
|
||||
*
|
||||
* @param profileId The token ID of the profile associated with the collectNFT being transferred.
|
||||
* @param pubId The publication ID associated with the collectNFT being transferred.
|
||||
* @param collectNFTId The collectNFT being transferred's token ID.
|
||||
* @param from The address the collectNFT is being transferred from.
|
||||
* @param to The address the collectNFT is being transferred to.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event CollectNFTTransferred(
|
||||
uint256 indexed profileId,
|
||||
uint256 indexed pubId,
|
||||
uint256 indexed collectNFTId,
|
||||
address from,
|
||||
address to,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
// Collect/Follow NFT-Specific
|
||||
|
||||
/**
|
||||
* @dev Emitted when a newly deployed follow NFT is initialized.
|
||||
*
|
||||
* @param profileId The token ID of the profile connected to this follow NFT.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event FollowNFTInitialized(uint256 profileId, uint256 timestamp);
|
||||
|
||||
/**
|
||||
* @dev Emitted when delegation power in a FollowNFT is changed.
|
||||
*
|
||||
* @param delegate The delegate whose power has been changed.
|
||||
* @param newPower The new governance power mapped to the delegate.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event FollowNFTDelegatedPowerChanged(address delegate, uint256 newPower, uint256 timestamp);
|
||||
|
||||
/**
|
||||
* @dev Emitted when a newly deployed collect NFT is initialized.
|
||||
*
|
||||
* @param profileId The token ID of the profile connected to the publication mapped to this collect NFT.
|
||||
* @param pubId The publication ID connected to the publication mapped to this collect NFT.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event CollectNFTInitialized(uint256 profileId, uint256 pubId, uint256 timestamp);
|
||||
|
||||
// Module-Specific
|
||||
|
||||
/**
|
||||
* @notice Emitted when the ModuleGlobals governance address is set.
|
||||
*
|
||||
* @param prevGovernance The previous governance address.
|
||||
* @param newGovernance The new governance address set.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event ModuleGlobalsGovernanceSet(
|
||||
address indexed prevGovernance,
|
||||
address indexed newGovernance,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
/**
|
||||
* @notice Emitted when the ModuleGlobals treasury address is set.
|
||||
*
|
||||
* @param prevTreasury The previous treasury address.
|
||||
* @param newTreasury The new treasury address set.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event ModuleGlobalsTreasurySet(
|
||||
address indexed prevTreasury,
|
||||
address indexed newTreasury,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
/**
|
||||
* @notice Emitted when the ModuleGlobals treasury fee is set.
|
||||
*
|
||||
* @param prevTreasuryFee The previous treasury fee in BPS.
|
||||
* @param newTreasuryFee The new treasury fee in BPS.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event ModuleGlobalsTreasuryFeeSet(
|
||||
uint16 indexed prevTreasuryFee,
|
||||
uint16 indexed newTreasuryFee,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
/**
|
||||
* @notice Emitted when a currency is added to or removed from the ModuleGlobals whitelist.
|
||||
*
|
||||
* @param currency The currency address.
|
||||
* @param prevWhitelisted Whether or not the currency was previously whitelisted.
|
||||
* @param whitelisted Whether or not the currency is whitelisted.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event ModuleGlobalsCurrencyWhitelisted(
|
||||
address indexed currency,
|
||||
bool indexed prevWhitelisted,
|
||||
bool indexed whitelisted,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
/**
|
||||
* @notice Emitted when a module inheriting from the `FeeModuleBase` is constructed.
|
||||
*
|
||||
* @param moduleGlobals The ModuleGlobals contract address used.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event FeeModuleBaseConstructed(address indexed moduleGlobals, uint256 timestamp);
|
||||
|
||||
/**
|
||||
* @notice Emitted when a module inheriting from the `ModuleBase` is constructed.
|
||||
*
|
||||
* @param hub The LensHub contract address used.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event ModuleBaseConstructed(address indexed hub, 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 grnated/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
|
||||
);
|
||||
}
|
||||
57
contracts/libraries/Helpers.sol
Normal file
57
contracts/libraries/Helpers.sol
Normal file
@@ -0,0 +1,57 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {DataTypes} from './DataTypes.sol';
|
||||
import {Errors} from './Errors.sol';
|
||||
|
||||
/**
|
||||
* @title Helpers
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This is a library that only contains a single function that is used in the hub contract as well as in
|
||||
* both the publishing logic and interaction logic libraries.
|
||||
*/
|
||||
library Helpers {
|
||||
/**
|
||||
* @notice This helper function just returns the pointed publication if the passed publication is a mirror,
|
||||
* otherwise it returns the passed publication.
|
||||
*
|
||||
* @param profileId The token ID of the profile that published the given publication.
|
||||
* @param pubId The publication ID of the given publication.
|
||||
* @param _pubByIdByProfile A pointer to the storage mapping of publications by pubId by profile ID.
|
||||
*
|
||||
* @return The pointed publication identifier if the the given publication is a mirror, otherwise, the given publication.
|
||||
* This is a tuple of (profileId, pubId, collectModule)
|
||||
*/
|
||||
function getPointedIfMirror(
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
mapping(uint256 => mapping(uint256 => DataTypes.PublicationStruct))
|
||||
storage _pubByIdByProfile
|
||||
)
|
||||
internal
|
||||
view
|
||||
returns (
|
||||
uint256,
|
||||
uint256,
|
||||
address
|
||||
)
|
||||
{
|
||||
address collectModule = _pubByIdByProfile[profileId][pubId].collectModule;
|
||||
if (collectModule != address(0)) {
|
||||
return (profileId, pubId, collectModule);
|
||||
} else {
|
||||
uint256 pointedTokenId = _pubByIdByProfile[profileId][pubId].profileIdPointed;
|
||||
// We validate existence here as an optimization, so validating in calling contracts is unnecessary
|
||||
if (pointedTokenId == 0) revert Errors.PublicationDoesNotExist();
|
||||
|
||||
uint256 pointedPubId = _pubByIdByProfile[profileId][pubId].pubIdPointed;
|
||||
|
||||
address pointedCollectModule = _pubByIdByProfile[pointedTokenId][pointedPubId]
|
||||
.collectModule;
|
||||
|
||||
return (pointedTokenId, pointedPubId, pointedCollectModule);
|
||||
}
|
||||
}
|
||||
}
|
||||
157
contracts/libraries/InteractionLogic.sol
Normal file
157
contracts/libraries/InteractionLogic.sol
Normal file
@@ -0,0 +1,157 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {Helpers} from './Helpers.sol';
|
||||
import {DataTypes} from './DataTypes.sol';
|
||||
import {Errors} from './Errors.sol';
|
||||
import {Events} from './Events.sol';
|
||||
import {Constants} from './Constants.sol';
|
||||
import {IFollowNFT} from '../interfaces/IFollowNFT.sol';
|
||||
import {ICollectNFT} from '../interfaces/ICollectNFT.sol';
|
||||
import {IFollowModule} from '../interfaces/IFollowModule.sol';
|
||||
import {ICollectModule} from '../interfaces/ICollectModule.sol';
|
||||
import {Clones} from '@openzeppelin/contracts/proxy/Clones.sol';
|
||||
import {Strings} from '@openzeppelin/contracts/utils/Strings.sol';
|
||||
|
||||
/**
|
||||
* @title InteractionLogic
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This is the library that contains the logic for follows & collects.
|
||||
|
||||
* @dev The functions are external, so they are called from the hub via `delegateCall` under the hood.
|
||||
*/
|
||||
library InteractionLogic {
|
||||
using Strings for uint256;
|
||||
|
||||
/**
|
||||
* @notice Follows the given profiles, executing the necessary logic and module calls before minting the follow
|
||||
* NFT(s) to the follower.
|
||||
*
|
||||
* @param follower The address executing the follow.
|
||||
* @param profileIds The array of profile token IDs to follow.
|
||||
* @param followModuleDatas The array of follow module data parameters to pass to each profile's follow module.
|
||||
* @param followNFTImpl The address of the follow NFT implementation, which has to be passed because it's an immutable in the hub.
|
||||
* @param _profileById A pointer to the storage mapping of profile structs by profile ID.
|
||||
*/
|
||||
function follow(
|
||||
address follower,
|
||||
uint256[] calldata profileIds,
|
||||
bytes[] calldata followModuleDatas,
|
||||
address followNFTImpl,
|
||||
mapping(uint256 => DataTypes.ProfileStruct) storage _profileById,
|
||||
mapping(bytes32 => uint256) storage _profileIdByHandleHash
|
||||
) external {
|
||||
if (profileIds.length != followModuleDatas.length) revert Errors.ArrayMismatch();
|
||||
for (uint256 i = 0; i < profileIds.length; i++) {
|
||||
string memory handle = _profileById[profileIds[i]].handle;
|
||||
if (_profileIdByHandleHash[keccak256(bytes(handle))] == 0) revert Errors.TokenDoesNotExist();
|
||||
|
||||
address followModule = _profileById[profileIds[i]].followModule;
|
||||
|
||||
address followNFT = _profileById[profileIds[i]].followNFT;
|
||||
|
||||
if (followNFT == address(0)) {
|
||||
followNFT = Clones.clone(followNFTImpl);
|
||||
_profileById[profileIds[i]].followNFT = followNFT;
|
||||
|
||||
bytes4 firstBytes = bytes4(bytes(handle));
|
||||
|
||||
string memory followNFTName = string(
|
||||
abi.encodePacked(handle, Constants.FOLLOW_NFT_NAME_SUFFIX)
|
||||
);
|
||||
string memory followNFTSymbol = string(
|
||||
abi.encodePacked(firstBytes, Constants.FOLLOW_NFT_SYMBOL_SUFFIX)
|
||||
);
|
||||
|
||||
IFollowNFT(followNFT).initialize(profileIds[i], followNFTName, followNFTSymbol);
|
||||
emit Events.FollowNFTDeployed(profileIds[i], followNFT, block.timestamp);
|
||||
}
|
||||
|
||||
IFollowNFT(followNFT).mint(follower);
|
||||
|
||||
if (followModule != address(0)) {
|
||||
IFollowModule(followModule).processFollow(
|
||||
follower,
|
||||
profileIds[i],
|
||||
followModuleDatas[i]
|
||||
);
|
||||
}
|
||||
}
|
||||
emit Events.Followed(follower, profileIds, block.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Collects the given publication, executing the necessary logic and module call before minting the
|
||||
* collect NFT to the collector.
|
||||
*
|
||||
* @param collector The address executing the collect.
|
||||
* @param profileId The token ID of the publication being collected's parent profile.
|
||||
* @param pubId The publication ID of the publication being collected.
|
||||
* @param collectModuleData The data to pass to the publication's collect module.
|
||||
* @param collectNFTImpl The address of the collect NFT implementation, which has to be passed because it's an immutable in the hub.
|
||||
* @param _pubByIdByProfile A pointer to the storage mapping of publications by pubId by profile ID.
|
||||
* @param _profileById A pointer to the storage mapping of profile structs by profile ID.
|
||||
*/
|
||||
function collect(
|
||||
address collector,
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata collectModuleData,
|
||||
address collectNFTImpl,
|
||||
mapping(uint256 => mapping(uint256 => DataTypes.PublicationStruct))
|
||||
storage _pubByIdByProfile,
|
||||
mapping(uint256 => DataTypes.ProfileStruct) storage _profileById
|
||||
) external {
|
||||
(uint256 rootProfileId, uint256 rootPubId, address rootCollectModule) = Helpers
|
||||
.getPointedIfMirror(profileId, pubId, _pubByIdByProfile);
|
||||
|
||||
address collectNFT = _pubByIdByProfile[rootProfileId][rootPubId].collectNFT;
|
||||
|
||||
if (collectNFT == address(0)) {
|
||||
collectNFT = Clones.clone(collectNFTImpl);
|
||||
_pubByIdByProfile[rootProfileId][rootPubId].collectNFT = collectNFT;
|
||||
|
||||
string memory handle = _profileById[rootProfileId].handle;
|
||||
bytes4 firstBytes = bytes4(bytes(handle));
|
||||
|
||||
string memory collectNFTName = string(
|
||||
abi.encodePacked(handle, Constants.COLLECT_NFT_NAME_INFIX, rootPubId.toString())
|
||||
);
|
||||
string memory collectNFTSymbol = string(
|
||||
abi.encodePacked(
|
||||
firstBytes,
|
||||
Constants.COLLECT_NFT_SYMBOL_INFIX,
|
||||
rootPubId.toString()
|
||||
)
|
||||
);
|
||||
|
||||
ICollectNFT(collectNFT).initialize(
|
||||
rootProfileId,
|
||||
rootPubId,
|
||||
collectNFTName,
|
||||
collectNFTSymbol
|
||||
);
|
||||
emit Events.CollectNFTDeployed(rootProfileId, rootPubId, collectNFT, block.timestamp);
|
||||
}
|
||||
|
||||
ICollectNFT(collectNFT).mint(collector);
|
||||
|
||||
ICollectModule(rootCollectModule).processCollect(
|
||||
profileId,
|
||||
collector,
|
||||
rootProfileId,
|
||||
rootPubId,
|
||||
collectModuleData
|
||||
);
|
||||
emit Events.Collected(
|
||||
collector,
|
||||
profileId,
|
||||
pubId,
|
||||
rootProfileId,
|
||||
rootPubId,
|
||||
block.timestamp
|
||||
);
|
||||
}
|
||||
}
|
||||
411
contracts/libraries/PublishingLogic.sol
Normal file
411
contracts/libraries/PublishingLogic.sol
Normal file
@@ -0,0 +1,411 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {Helpers} from './Helpers.sol';
|
||||
import {DataTypes} from './DataTypes.sol';
|
||||
import {Errors} from './Errors.sol';
|
||||
import {Events} from './Events.sol';
|
||||
import {Constants} from './Constants.sol';
|
||||
import {IFollowModule} from '../interfaces/IFollowModule.sol';
|
||||
import {ICollectModule} from '../interfaces/ICollectModule.sol';
|
||||
import {IReferenceModule} from '../interfaces/IReferenceModule.sol';
|
||||
|
||||
/**
|
||||
* @title PublishingLogic
|
||||
* @author Lens
|
||||
*
|
||||
* @notice This is the library that contains the logic for profile creation & publication.
|
||||
*
|
||||
* @dev The functions are external, so they are called from the hub via `delegateCall` under the hood. Furthermore,
|
||||
* expected events are emitted from this library instead of from the hub to alleviate code size concerns.
|
||||
*/
|
||||
library PublishingLogic {
|
||||
/**
|
||||
* @notice Executes the logic to create a profile with the given parameters to the given address.
|
||||
*
|
||||
* @param vars The CreateProfileData struct containing the following parameters:
|
||||
* to: The address receiving the profile.
|
||||
* handle: The handle to set for the profile, must be unique and non-empty.
|
||||
* imageURI: The URI to set for the profile image.
|
||||
* followModule: The follow module to use, can be the zero address.
|
||||
* followModuleData: The follow module initialization data, if any
|
||||
* followNFTURI: The URI to set for the follow NFT.
|
||||
* @param profileId The profile ID to associate with this profile NFT (token ID).
|
||||
* @param _profileIdByHandleHash The storage reference to the mapping of profile IDs by handle hash.
|
||||
* @param _profileById The storage reference to the mapping of profile structs by IDs.
|
||||
* @param _followModuleWhitelisted The storage reference to the mapping of whitelist status by follow module address.
|
||||
*/
|
||||
function createProfile(
|
||||
DataTypes.CreateProfileData calldata vars,
|
||||
uint256 profileId,
|
||||
mapping(bytes32 => uint256) storage _profileIdByHandleHash,
|
||||
mapping(uint256 => DataTypes.ProfileStruct) storage _profileById,
|
||||
mapping(address => bool) storage _followModuleWhitelisted
|
||||
) external {
|
||||
_validateHandle(vars.handle);
|
||||
|
||||
bytes32 handleHash = keccak256(bytes(vars.handle));
|
||||
|
||||
if (_profileIdByHandleHash[handleHash] != 0) revert Errors.HandleTaken();
|
||||
_profileIdByHandleHash[handleHash] = profileId;
|
||||
|
||||
_profileById[profileId].handle = vars.handle;
|
||||
_profileById[profileId].imageURI = vars.imageURI;
|
||||
_profileById[profileId].followNFTURI = vars.followNFTURI;
|
||||
|
||||
if (vars.followModule != address(0)) {
|
||||
_profileById[profileId].followModule = vars.followModule;
|
||||
}
|
||||
|
||||
bytes memory followModuleReturnData = _initFollowModule(
|
||||
profileId,
|
||||
vars.followModule,
|
||||
vars.followModuleData,
|
||||
_followModuleWhitelisted
|
||||
);
|
||||
|
||||
_emitProfileCreated(profileId, vars, followModuleReturnData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Sets the follow module for a given profile.
|
||||
*
|
||||
* @param profileId The profile ID to set the follow module for.
|
||||
* @param followModule The follow module to set for the given profile, if any.
|
||||
* @param followModuleData The data to pass to the follow module for profile initialization.
|
||||
* @param _profile The storage reference to the profile struct associated with the given profile ID.
|
||||
* @param _followModuleWhitelisted The storage reference to the mapping of whitelist status by follow module address.
|
||||
*/
|
||||
function setFollowModule(
|
||||
uint256 profileId,
|
||||
address followModule,
|
||||
bytes calldata followModuleData,
|
||||
DataTypes.ProfileStruct storage _profile,
|
||||
mapping(address => bool) storage _followModuleWhitelisted
|
||||
) external {
|
||||
address prevFollowModule = _profile.followModule;
|
||||
if (followModule != prevFollowModule) {
|
||||
_profile.followModule = followModule;
|
||||
}
|
||||
|
||||
bytes memory followModuleReturnData = _initFollowModule(
|
||||
profileId,
|
||||
followModule,
|
||||
followModuleData,
|
||||
_followModuleWhitelisted
|
||||
);
|
||||
emit Events.FollowModuleSet(
|
||||
profileId,
|
||||
followModule,
|
||||
followModuleReturnData,
|
||||
block.timestamp
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Creates a post publication mapped to the given profile.
|
||||
*
|
||||
* @dev To avoid a stack too deep error, reference parameters are passed in memory rather than calldata.
|
||||
*
|
||||
* @param profileId The profile ID to associate this publication to.
|
||||
* @param contentURI The URI to set for this publication.
|
||||
* @param collectModule The collect module to set for this publication.
|
||||
* @param collectModuleData The data to pass to the collect module for publication initialization.
|
||||
* @param referenceModule The reference module to set for this publication, if any.
|
||||
* @param referenceModuleData The data to pass to the reference module for publication initialization.
|
||||
* @param pubId The publication ID to associate with this publication.
|
||||
* @param _pubByIdByProfile The storage reference to the mapping of publications by publication ID by profile ID.
|
||||
* @param _collectModuleWhitelisted The storage reference to the mapping of whitelist status by collect module address.
|
||||
* @param _referenceModuleWhitelisted The storage reference to the mapping of whitelist status by reference module address.
|
||||
*/
|
||||
function createPost(
|
||||
uint256 profileId,
|
||||
string memory contentURI,
|
||||
address collectModule,
|
||||
bytes memory collectModuleData,
|
||||
address referenceModule,
|
||||
bytes memory referenceModuleData,
|
||||
uint256 pubId,
|
||||
mapping(uint256 => mapping(uint256 => DataTypes.PublicationStruct))
|
||||
storage _pubByIdByProfile,
|
||||
mapping(address => bool) storage _collectModuleWhitelisted,
|
||||
mapping(address => bool) storage _referenceModuleWhitelisted
|
||||
) external {
|
||||
_pubByIdByProfile[profileId][pubId].contentURI = contentURI;
|
||||
|
||||
// Collect module initialization
|
||||
bytes memory collectModuleReturnData = _initPubCollectModule(
|
||||
profileId,
|
||||
pubId,
|
||||
collectModule,
|
||||
collectModuleData,
|
||||
_pubByIdByProfile,
|
||||
_collectModuleWhitelisted
|
||||
);
|
||||
|
||||
// Reference module initialization
|
||||
bytes memory referenceModuleReturnData = _initPubReferenceModule(
|
||||
profileId,
|
||||
pubId,
|
||||
referenceModule,
|
||||
referenceModuleData,
|
||||
_pubByIdByProfile,
|
||||
_referenceModuleWhitelisted
|
||||
);
|
||||
|
||||
emit Events.PostCreated(
|
||||
profileId,
|
||||
pubId,
|
||||
contentURI,
|
||||
collectModule,
|
||||
collectModuleReturnData,
|
||||
referenceModule,
|
||||
referenceModuleReturnData,
|
||||
block.timestamp
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Creates a comment publication mapped to the given profile.
|
||||
*
|
||||
* @dev This function is unique in that it requires many variables, so, unlike the other publishing functions,
|
||||
* we need to pass the full CommentData struct in memory to avoid a stack too deep error.
|
||||
*
|
||||
* @param vars The CommentData struct to use to create the comment.
|
||||
* @param pubId The publication ID to associate with this publication.
|
||||
* @param _profileById The storage reference to the mapping of profile structs by IDs.
|
||||
* @param _pubByIdByProfile The storage reference to the mapping of publications by publication ID by profile ID.
|
||||
* @param _collectModuleWhitelisted The storage reference to the mapping of whitelist status by collect module address.
|
||||
* @param _referenceModuleWhitelisted The storage reference to the mapping of whitelist status by reference module address.
|
||||
*/
|
||||
function createComment(
|
||||
DataTypes.CommentData memory vars,
|
||||
uint256 pubId,
|
||||
mapping(uint256 => DataTypes.ProfileStruct) storage _profileById,
|
||||
mapping(uint256 => mapping(uint256 => DataTypes.PublicationStruct))
|
||||
storage _pubByIdByProfile,
|
||||
mapping(address => bool) storage _collectModuleWhitelisted,
|
||||
mapping(address => bool) storage _referenceModuleWhitelisted
|
||||
) external {
|
||||
// Validate existence of the pointed publication
|
||||
uint256 pubCount = _profileById[vars.profileIdPointed].pubCount;
|
||||
if (pubCount < vars.pubIdPointed || vars.pubIdPointed == 0)
|
||||
revert Errors.PublicationDoesNotExist();
|
||||
|
||||
_pubByIdByProfile[vars.profileId][pubId].contentURI = vars.contentURI;
|
||||
_pubByIdByProfile[vars.profileId][pubId].profileIdPointed = vars.profileIdPointed;
|
||||
_pubByIdByProfile[vars.profileId][pubId].pubIdPointed = vars.pubIdPointed;
|
||||
|
||||
// Collect Module Initialization
|
||||
bytes memory collectModuleReturnData = _initPubCollectModule(
|
||||
vars.profileId,
|
||||
pubId,
|
||||
vars.collectModule,
|
||||
vars.collectModuleData,
|
||||
_pubByIdByProfile,
|
||||
_collectModuleWhitelisted
|
||||
);
|
||||
|
||||
// Reference module initialization
|
||||
bytes memory referenceModuleReturnData = _initPubReferenceModule(
|
||||
vars.profileId,
|
||||
pubId,
|
||||
vars.referenceModule,
|
||||
vars.referenceModuleData,
|
||||
_pubByIdByProfile,
|
||||
_referenceModuleWhitelisted
|
||||
);
|
||||
|
||||
// Reference module validation
|
||||
address refModule = _pubByIdByProfile[vars.profileIdPointed][vars.pubIdPointed]
|
||||
.referenceModule;
|
||||
if (refModule != address(0)) {
|
||||
IReferenceModule(refModule).processComment(
|
||||
vars.profileId,
|
||||
vars.profileIdPointed,
|
||||
vars.pubIdPointed
|
||||
);
|
||||
}
|
||||
|
||||
// Prevents a stack too deep error
|
||||
_emitCommentCreated(vars, pubId, collectModuleReturnData, referenceModuleReturnData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Creates a mirror publication mapped to the given profile.
|
||||
*
|
||||
* @param profileId The profile ID to associate this publication to.
|
||||
* @param profileIdPointed The profile ID of the pointed publication's publisher.
|
||||
* @param pubIdPointed The pointed publication's publication ID.
|
||||
* @param referenceModule The reference module to set for this publication, if any.
|
||||
* @param referenceModuleData The data to pass to the reference module for publication initialization.
|
||||
* @param pubId The publication ID to associate with this publication.
|
||||
* @param _pubByIdByProfile The storage reference to the mapping of publications by publication ID by profile ID.
|
||||
* @param _referenceModuleWhitelisted The storage reference to the mapping of whitelist status by reference module address.
|
||||
*/
|
||||
function createMirror(
|
||||
uint256 profileId,
|
||||
uint256 profileIdPointed,
|
||||
uint256 pubIdPointed,
|
||||
address referenceModule,
|
||||
bytes calldata referenceModuleData,
|
||||
uint256 pubId,
|
||||
mapping(uint256 => mapping(uint256 => DataTypes.PublicationStruct))
|
||||
storage _pubByIdByProfile,
|
||||
mapping(address => bool) storage _referenceModuleWhitelisted
|
||||
) external {
|
||||
(uint256 rootProfileIdPointed, uint256 rootPubIdPointed, ) = Helpers.getPointedIfMirror(
|
||||
profileIdPointed,
|
||||
pubIdPointed,
|
||||
_pubByIdByProfile
|
||||
);
|
||||
|
||||
_pubByIdByProfile[profileId][pubId].profileIdPointed = rootProfileIdPointed;
|
||||
_pubByIdByProfile[profileId][pubId].pubIdPointed = rootPubIdPointed;
|
||||
|
||||
// Reference module initialization
|
||||
bytes memory referenceModuleReturnData = _initPubReferenceModule(
|
||||
profileId,
|
||||
pubId,
|
||||
referenceModule,
|
||||
referenceModuleData,
|
||||
_pubByIdByProfile,
|
||||
_referenceModuleWhitelisted
|
||||
);
|
||||
|
||||
// Reference module validation
|
||||
address refModule = _pubByIdByProfile[rootProfileIdPointed][rootPubIdPointed]
|
||||
.referenceModule;
|
||||
if (refModule != address(0)) {
|
||||
IReferenceModule(refModule).processMirror(
|
||||
profileId,
|
||||
rootProfileIdPointed,
|
||||
rootPubIdPointed
|
||||
);
|
||||
}
|
||||
|
||||
emit Events.MirrorCreated(
|
||||
profileId,
|
||||
pubId,
|
||||
rootProfileIdPointed,
|
||||
rootPubIdPointed,
|
||||
referenceModule,
|
||||
referenceModuleReturnData,
|
||||
block.timestamp
|
||||
);
|
||||
}
|
||||
|
||||
function _initPubCollectModule(
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
address collectModule,
|
||||
bytes memory collectModuleData,
|
||||
mapping(uint256 => mapping(uint256 => DataTypes.PublicationStruct))
|
||||
storage _pubByIdByProfile,
|
||||
mapping(address => bool) storage _collectModuleWhitelisted
|
||||
) private returns (bytes memory) {
|
||||
if (!_collectModuleWhitelisted[collectModule]) revert Errors.CollectModuleNotWhitelisted();
|
||||
_pubByIdByProfile[profileId][pubId].collectModule = collectModule;
|
||||
return
|
||||
ICollectModule(collectModule).initializePublicationCollectModule(
|
||||
profileId,
|
||||
pubId,
|
||||
collectModuleData
|
||||
);
|
||||
}
|
||||
|
||||
function _initPubReferenceModule(
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
address referenceModule,
|
||||
bytes memory referenceModuleData,
|
||||
mapping(uint256 => mapping(uint256 => DataTypes.PublicationStruct))
|
||||
storage _pubByIdByProfile,
|
||||
mapping(address => bool) storage _referenceModuleWhitelisted
|
||||
) private returns (bytes memory) {
|
||||
if (referenceModule != address(0)) {
|
||||
if (!_referenceModuleWhitelisted[referenceModule])
|
||||
revert Errors.ReferenceModuleNotWhitelisted();
|
||||
_pubByIdByProfile[profileId][pubId].referenceModule = referenceModule;
|
||||
return
|
||||
IReferenceModule(referenceModule).initializeReferenceModule(
|
||||
profileId,
|
||||
pubId,
|
||||
referenceModuleData
|
||||
);
|
||||
} else {
|
||||
return new bytes(0);
|
||||
}
|
||||
}
|
||||
|
||||
function _initFollowModule(
|
||||
uint256 profileId,
|
||||
address followModule,
|
||||
bytes memory followModuleData,
|
||||
mapping(address => bool) storage _followModuleWhitelisted
|
||||
) private returns (bytes memory) {
|
||||
if (followModule != address(0)) {
|
||||
if (!_followModuleWhitelisted[followModule]) revert Errors.FollowModuleNotWhitelisted();
|
||||
bytes memory returnData = IFollowModule(followModule).initializeFollowModule(
|
||||
profileId,
|
||||
followModuleData
|
||||
);
|
||||
return returnData;
|
||||
} else {
|
||||
return new bytes(0);
|
||||
}
|
||||
}
|
||||
|
||||
function _emitCommentCreated(
|
||||
DataTypes.CommentData memory vars,
|
||||
uint256 pubId,
|
||||
bytes memory collectModuleReturnData,
|
||||
bytes memory referenceModuleReturnData
|
||||
) private {
|
||||
emit Events.CommentCreated(
|
||||
vars.profileId,
|
||||
pubId,
|
||||
vars.contentURI,
|
||||
vars.profileIdPointed,
|
||||
vars.pubIdPointed,
|
||||
vars.collectModule,
|
||||
collectModuleReturnData,
|
||||
vars.referenceModule,
|
||||
referenceModuleReturnData,
|
||||
block.timestamp
|
||||
);
|
||||
}
|
||||
|
||||
function _emitProfileCreated(
|
||||
uint256 profileId,
|
||||
DataTypes.CreateProfileData calldata vars,
|
||||
bytes memory followModuleReturnData
|
||||
) internal {
|
||||
emit Events.ProfileCreated(
|
||||
profileId,
|
||||
msg.sender, // Creator is always the msg sender
|
||||
vars.to,
|
||||
vars.handle,
|
||||
vars.imageURI,
|
||||
vars.followModule,
|
||||
followModuleReturnData,
|
||||
vars.followNFTURI,
|
||||
block.timestamp
|
||||
);
|
||||
}
|
||||
|
||||
function _validateHandle(string calldata handle) private pure {
|
||||
bytes memory byteHandle = bytes(handle);
|
||||
if (byteHandle.length == 0 || byteHandle.length > Constants.MAX_HANDLE_LENGTH)
|
||||
revert Errors.HandleLengthInvalid();
|
||||
|
||||
for (uint256 i = 0; i < byteHandle.length; i++) {
|
||||
if (
|
||||
(byteHandle[i] < '0' ||
|
||||
byteHandle[i] > 'z' ||
|
||||
(byteHandle[i] > '9' && byteHandle[i] < 'a')) && byteHandle[i] != '.'
|
||||
) revert Errors.HandleContainsInvalidCharacters();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
contracts/mocks/Currency.sol
Normal file
11
contracts/mocks/Currency.sol
Normal file
@@ -0,0 +1,11 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol';
|
||||
|
||||
contract Currency is ERC20('Currency', 'CRNC') {
|
||||
function mint(address to, uint256 amount) external {
|
||||
_mint(to, amount);
|
||||
}
|
||||
}
|
||||
32
contracts/mocks/Helper.sol
Normal file
32
contracts/mocks/Helper.sol
Normal file
@@ -0,0 +1,32 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {IFollowNFT} from '../interfaces/IFollowNFT.sol';
|
||||
|
||||
/**
|
||||
* @dev This is a helper contract used for internal testing.
|
||||
*
|
||||
* NOTE: This contract is not meant to be deployed and is unsafe for use.
|
||||
*/
|
||||
contract Helper {
|
||||
/**
|
||||
* @dev This is a helper function that exposes the block number due to the inconsistency of
|
||||
* fetching the block number from scripts.
|
||||
*/
|
||||
function getBlockNumber() external view returns (uint256) {
|
||||
return block.number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev This is a helper function to aid in testing same-block delegation in the FollowNFT contract.
|
||||
*/
|
||||
function batchDelegate(
|
||||
IFollowNFT nft,
|
||||
address first,
|
||||
address second
|
||||
) external {
|
||||
nft.delegate(first);
|
||||
nft.delegate(second);
|
||||
}
|
||||
}
|
||||
37
contracts/mocks/MockFollowModule.sol
Normal file
37
contracts/mocks/MockFollowModule.sol
Normal file
@@ -0,0 +1,37 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {IFollowModule} from '../interfaces/IFollowModule.sol';
|
||||
|
||||
contract MockFollowModule is IFollowModule {
|
||||
function initializeFollowModule(uint256 profileId, bytes calldata data)
|
||||
external
|
||||
pure
|
||||
override
|
||||
returns (bytes memory)
|
||||
{
|
||||
uint256 number = abi.decode(data, (uint256));
|
||||
require(number == 1, 'MockFollowModule: invalid');
|
||||
return new bytes(0);
|
||||
}
|
||||
|
||||
function processFollow(
|
||||
address follower,
|
||||
uint256 profileId,
|
||||
bytes calldata data
|
||||
) external override {}
|
||||
|
||||
function validateFollow(
|
||||
uint256 profileId,
|
||||
address follower,
|
||||
uint256 followNFTTokenId
|
||||
) external view override {}
|
||||
|
||||
function followModuleTransferHook(
|
||||
uint256 profileId,
|
||||
address from,
|
||||
address to,
|
||||
uint256 followNFTTokenId
|
||||
) external override {}
|
||||
}
|
||||
44
contracts/mocks/MockLensHubV2.sol
Normal file
44
contracts/mocks/MockLensHubV2.sol
Normal file
@@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {ILensHub} from '../interfaces/ILensHub.sol';
|
||||
import {Events} from '../libraries/Events.sol';
|
||||
import {Helpers} from '../libraries/Helpers.sol';
|
||||
import {DataTypes} from '../libraries/DataTypes.sol';
|
||||
import {Errors} from '../libraries/Errors.sol';
|
||||
import {PublishingLogic} from '../libraries/PublishingLogic.sol';
|
||||
import {InteractionLogic} from '../libraries/InteractionLogic.sol';
|
||||
import {LensNFTBase} from '../core/base/LensNFTBase.sol';
|
||||
import {LensMultiState} from '../core/base/LensMultiState.sol';
|
||||
import {VersionedInitializable} from '../upgradeability/VersionedInitializable.sol';
|
||||
import {MockLensHubV2Storage} from './MockLensHubV2Storage.sol';
|
||||
|
||||
/**
|
||||
* @dev A mock upgraded LensHub contract that is used mainly to validate that the initializer works as expected and
|
||||
* that the storage layout after an upgrade is valid.
|
||||
*/
|
||||
contract MockLensHubV2 is
|
||||
LensNFTBase,
|
||||
VersionedInitializable,
|
||||
LensMultiState,
|
||||
MockLensHubV2Storage
|
||||
{
|
||||
uint256 internal constant REVISION = 2;
|
||||
|
||||
function initialize(uint256 newValue) external initializer {
|
||||
_additionalValue = newValue;
|
||||
}
|
||||
|
||||
function setAdditionalValue(uint256 newValue) external {
|
||||
_additionalValue = newValue;
|
||||
}
|
||||
|
||||
function getAdditionalValue() external view returns (uint256) {
|
||||
return _additionalValue;
|
||||
}
|
||||
|
||||
function getRevision() internal pure virtual override returns (uint256) {
|
||||
return REVISION;
|
||||
}
|
||||
}
|
||||
43
contracts/mocks/MockLensHubV2BadRevision.sol
Normal file
43
contracts/mocks/MockLensHubV2BadRevision.sol
Normal file
@@ -0,0 +1,43 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {ILensHub} from '../interfaces/ILensHub.sol';
|
||||
import {Events} from '../libraries/Events.sol';
|
||||
import {Helpers} from '../libraries/Helpers.sol';
|
||||
import {DataTypes} from '../libraries/DataTypes.sol';
|
||||
import {Errors} from '../libraries/Errors.sol';
|
||||
import {PublishingLogic} from '../libraries/PublishingLogic.sol';
|
||||
import {InteractionLogic} from '../libraries/InteractionLogic.sol';
|
||||
import {LensNFTBase} from '../core/base/LensNFTBase.sol';
|
||||
import {LensMultiState} from '../core/base/LensMultiState.sol';
|
||||
import {VersionedInitializable} from '../upgradeability/VersionedInitializable.sol';
|
||||
import {MockLensHubV2Storage} from './MockLensHubV2Storage.sol';
|
||||
|
||||
/**
|
||||
* @dev A mock upgraded LensHub contract that is used to validate that the initializer cannot be called with the same revision.
|
||||
*/
|
||||
contract MockLensHubV2BadRevision is
|
||||
LensNFTBase,
|
||||
VersionedInitializable,
|
||||
LensMultiState,
|
||||
MockLensHubV2Storage
|
||||
{
|
||||
uint256 internal constant REVISION = 1; // Should fail the initializer check
|
||||
|
||||
function initialize(uint256 newValue) external initializer {
|
||||
_additionalValue = newValue;
|
||||
}
|
||||
|
||||
function setAdditionalValue(uint256 newValue) external {
|
||||
_additionalValue = newValue;
|
||||
}
|
||||
|
||||
function getAdditionalValue() external view returns (uint256) {
|
||||
return _additionalValue;
|
||||
}
|
||||
|
||||
function getRevision() internal pure virtual override returns (uint256) {
|
||||
return REVISION;
|
||||
}
|
||||
}
|
||||
66
contracts/mocks/MockLensHubV2Storage.sol
Normal file
66
contracts/mocks/MockLensHubV2Storage.sol
Normal file
@@ -0,0 +1,66 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {DataTypes} from '../libraries/DataTypes.sol';
|
||||
|
||||
contract MockLensHubV2Storage {
|
||||
bytes32 internal constant CREATE_PROFILE_WITH_SIG_TYPEHASH =
|
||||
0x9ac3269d9abd6f8c5e850e07f21b199079e8a5cc4a55466d8c96ab0c4a5be403;
|
||||
// keccak256(
|
||||
// 'CreateProfileWithSig(string handle,string uri,address followModule,bytes followModuleData,uint256 nonce,uint256 deadline)'
|
||||
// );
|
||||
bytes32 internal constant SET_FOLLOW_MODULE_WITH_SIG_TYPEHASH =
|
||||
0x6f3f6455a608af1cc57ef3e5c0a49deeb88bba264ec8865b798ff07358859d4b;
|
||||
// keccak256(
|
||||
// 'SetFollowModuleWithSig(uint256 profileId,address followModule,bytes followModuleData,uint256 nonce,uint256 deadline)'
|
||||
// );
|
||||
bytes32 internal constant SET_DISPATCHER_WITH_SIG_TYPEHASH =
|
||||
0x77ba3e9f5fa75343bbad1241fb539a0064de97694b47d463d1eb5c54aba11f0f;
|
||||
// keccak256(
|
||||
// 'SetDispatcherWithSig(uint256 profileId,address dispatcher,uint256 nonce,uint256 deadline)'
|
||||
// );
|
||||
bytes32 internal constant SET_PROFILE_IMAGE_URI_WITH_SIG_TYPEHASH =
|
||||
0x5b9860bd835e648945b22d053515bc1f53b7d9fab4b23b1b49db15722e945d14;
|
||||
// keccak256(
|
||||
// 'SetProfileImageURIWithSig(uint256 profileId,string imageURI,uint256 nonce,uint256 deadline)'
|
||||
// );
|
||||
bytes32 internal constant POST_WITH_SIG_TYPEHASH =
|
||||
0xfb8f057542e7551386ead0b891a45f102af78c47f8cc58b4a919c7cfeccd0e1e;
|
||||
// keccak256(
|
||||
// 'PostWithSig(uint256 profileId,string contentURI,address collectModule,bytes collectModuleData,address referenceModule,bytes referenceModuleData,uint256 nonce,uint256 deadline)'
|
||||
// );
|
||||
bytes32 internal constant COMMENT_WITH_SIG_TYPEHASH =
|
||||
0xb30910150df56294e05b2d03e181803697a2b935abb1b9bdddde9310f618fe9b;
|
||||
// keccak256(
|
||||
// 'CommentWithSig(uint256 profileId,string contentURI,uint256 profileIdPointed,uint256 pubIdPointed,address collectModule,bytes collectModuleData,address referenceModule,bytes referenceModuleData,uint256 nonce,uint256 deadline)'
|
||||
// );
|
||||
bytes32 internal constant MIRROR_WITH_SIG_TYPEHASH =
|
||||
0x64f4578fc098f96a2450fbe601cb8c5318ebeb2ff72d2031a36be1ff6932d5ee;
|
||||
// keccak256(
|
||||
// 'MirrorWithSig(uint256 profileId,uint256 profileIdPointed,uint256 pubIdPointed,address referenceModule,bytes referenceModuleData,uint256 nonce,uint256 deadline)'
|
||||
// );
|
||||
bytes32 internal constant FOLLOW_WITH_SIG_TYPEHASH =
|
||||
0xfb6b7f1cd1b38daf3822aff0abbe78124db5d62a4748bcff007c15ccd6d30bc5;
|
||||
// keccak256(
|
||||
// 'FollowWithSig(uint256[] profileIds,bytes[] datas,uint256 nonce,uint256 deadline)'
|
||||
// );
|
||||
bytes32 internal constant COLLECT_WITH_SIG_TYPEHASH =
|
||||
0x7f9b4ea1fc678b4fda1611ac5cbd28f339e235d89b1540635e9b2e0223a3c101;
|
||||
// keccak256(
|
||||
// 'CollectWithSig(uint256 profileId,uint256 pubId,bytes data,uint256 nonce,uint256 deadline)'
|
||||
// );
|
||||
|
||||
mapping(address => bool) internal _followModuleWhitelisted;
|
||||
mapping(address => bool) internal _collectModuleWhitelisted;
|
||||
mapping(address => bool) internal _referenceModuleWhitelisted;
|
||||
|
||||
mapping(uint256 => address) internal _dispatcherByProfile;
|
||||
mapping(bytes32 => uint256) internal _profileIdByHandleHash;
|
||||
mapping(uint256 => DataTypes.ProfileStruct) internal _profileById;
|
||||
mapping(uint256 => mapping(uint256 => DataTypes.PublicationStruct)) internal _pubByIdByProfile;
|
||||
|
||||
uint256 internal _profileCounter;
|
||||
address internal _governance;
|
||||
address internal _emergencyAdmin;
|
||||
uint256 internal _additionalValue;
|
||||
}
|
||||
29
contracts/mocks/MockReferenceModule.sol
Normal file
29
contracts/mocks/MockReferenceModule.sol
Normal file
@@ -0,0 +1,29 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {IReferenceModule} from '../interfaces/IReferenceModule.sol';
|
||||
|
||||
contract MockReferenceModule is IReferenceModule {
|
||||
function initializeReferenceModule(
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata data
|
||||
) external pure override returns (bytes memory) {
|
||||
uint256 number = abi.decode(data, (uint256));
|
||||
require(number == 1, 'MockReferenceModule: invalid');
|
||||
return new bytes(0);
|
||||
}
|
||||
|
||||
function processComment(
|
||||
uint256 profileId,
|
||||
uint256 profileIdPointed,
|
||||
uint256 pubIdPointed
|
||||
) external override {}
|
||||
|
||||
function processMirror(
|
||||
uint256 profileId,
|
||||
uint256 profileIdPointed,
|
||||
uint256 pubIdPointed
|
||||
) external override {}
|
||||
}
|
||||
5
contracts/upgradeability/ProxyBase.sol
Normal file
5
contracts/upgradeability/ProxyBase.sol
Normal file
@@ -0,0 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {TransparentUpgradeableProxy} from '@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol';
|
||||
51
contracts/upgradeability/VersionedInitializable.sol
Normal file
51
contracts/upgradeability/VersionedInitializable.sol
Normal file
@@ -0,0 +1,51 @@
|
||||
// SPDX-License-Identifier: agpl-3.0
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {Errors} from '../libraries/Errors.sol';
|
||||
|
||||
/**
|
||||
* @title VersionedInitializable
|
||||
*
|
||||
* @dev Helper contract to implement initializer functions. To use it, replace
|
||||
* the constructor with a function that has the `initializer` modifier.
|
||||
* WARNING: Unlike constructors, initializer functions must be manually
|
||||
* invoked. This applies both to deploying an Initializable contract, as well
|
||||
* as extending an Initializable contract via inheritance.
|
||||
* WARNING: When used with inheritance, manual care must be taken to not invoke
|
||||
* a parent initializer twice, or ensure that all initializers are idempotent,
|
||||
* because this is not dealt with automatically as with constructors.
|
||||
*
|
||||
* This is slightly modified from [Aave's version.](https://github.com/aave/protocol-v2/blob/6a503eb0a897124d8b9d126c915ffdf3e88343a9/contracts/protocol/libraries/aave-upgradeability/VersionedInitializable.sol)
|
||||
*
|
||||
* @author Lens, inspired by Aave's implementation, which is in turn inspired by OpenZeppelin's
|
||||
* Initializable contract
|
||||
*/
|
||||
abstract contract VersionedInitializable {
|
||||
address private immutable originalImpl;
|
||||
|
||||
/**
|
||||
* @dev Indicates that the contract has been initialized.
|
||||
*/
|
||||
uint256 private lastInitializedRevision = 0;
|
||||
|
||||
/**
|
||||
* @dev Modifier to use in the initializer function of a contract.
|
||||
*/
|
||||
modifier initializer() {
|
||||
uint256 revision = getRevision();
|
||||
if (address(this) == originalImpl) revert Errors.CannotInitImplementation();
|
||||
if (revision <= lastInitializedRevision) revert Errors.Initialized();
|
||||
lastInitializedRevision = revision;
|
||||
_;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
originalImpl = address(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev returns the revision number of the contract
|
||||
* Needs to be defined in the inherited class as a constant.
|
||||
**/
|
||||
function getRevision() internal pure virtual returns (uint256);
|
||||
}
|
||||
23
docker-compose.yml
Normal file
23
docker-compose.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
version: '3.5'
|
||||
|
||||
services:
|
||||
contracts-env:
|
||||
env_file:
|
||||
- .env
|
||||
build:
|
||||
context: ./
|
||||
working_dir: /src
|
||||
command: npm run run-env
|
||||
volumes:
|
||||
- ./:/src
|
||||
- $HOME/.tenderly/config.yaml:/root/.tenderly/config.yaml
|
||||
environment:
|
||||
MNEMONIC: ${MNEMONIC}
|
||||
ETHERSCAN_KEY: ${ETHERSCAN_KEY}
|
||||
INFURA_KEY: ${INFURA_KEY}
|
||||
ETHERSCAN_NETWORK: ${ETHERSCAN_NETWORK}
|
||||
TENDERLY_PROJECT: ${TENDERLY_PROJECT}
|
||||
TENDERLY_USERNAME: ${TENDERLY_USERNAME}
|
||||
ALCHEMY_KEY: ${ALCHEMY_KEY}
|
||||
TENDERLY_FORK_ID: ${TENDERLY_FORK_ID}
|
||||
TENDERLY_HEAD_ID: ${TENDERLY_HEAD_ID}
|
||||
105
hardhat.config.ts
Normal file
105
hardhat.config.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { HardhatUserConfig } from 'hardhat/types';
|
||||
import { accounts } from './helpers/test-wallets';
|
||||
import { eEthereumNetwork, eNetwork, ePolygonNetwork, eXDaiNetwork } from './helpers/types';
|
||||
import { HARDHATEVM_CHAINID } from './helpers/hardhat-constants';
|
||||
import { NETWORKS_RPC_URL, NETWORKS_DEFAULT_GAS } from './helper-hardhat-config';
|
||||
import dotenv from 'dotenv';
|
||||
import glob from 'glob';
|
||||
import path from 'path';
|
||||
dotenv.config({ path: '../.env' });
|
||||
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import '@nomiclabs/hardhat-etherscan';
|
||||
import '@typechain/hardhat';
|
||||
import 'solidity-coverage';
|
||||
import 'hardhat-gas-reporter';
|
||||
import 'hardhat-contract-sizer';
|
||||
import 'hardhat-log-remover';
|
||||
import 'hardhat-spdx-license-identifier';
|
||||
|
||||
if (!process.env.SKIP_LOAD) {
|
||||
glob.sync('./tasks/**/*.ts').forEach(function (file) {
|
||||
require(path.resolve(file));
|
||||
});
|
||||
}
|
||||
|
||||
const DEFAULT_BLOCK_GAS_LIMIT = 12450000;
|
||||
const DEFAULT_GAS_MUL = 5;
|
||||
const HARDFORK = 'london';
|
||||
const MNEMONIC_PATH = "m/44'/60'/0'/0";
|
||||
const MNEMONIC = process.env.MNEMONIC || '';
|
||||
const MAINNET_FORK = process.env.MAINNET_FORK === 'true';
|
||||
const TRACK_GAS = process.env.TRACK_GAS === 'true';
|
||||
|
||||
const getCommonNetworkConfig = (networkName: eNetwork, networkId: number) => ({
|
||||
url: NETWORKS_RPC_URL[networkName],
|
||||
hardfork: HARDFORK,
|
||||
blockGasLimit: DEFAULT_BLOCK_GAS_LIMIT,
|
||||
gasMultiplier: DEFAULT_GAS_MUL,
|
||||
gasPrice: NETWORKS_DEFAULT_GAS[networkName],
|
||||
chainId: networkId,
|
||||
accounts: {
|
||||
mnemonic: MNEMONIC,
|
||||
path: MNEMONIC_PATH,
|
||||
initialIndex: 0,
|
||||
count: 20,
|
||||
},
|
||||
});
|
||||
|
||||
const mainnetFork = MAINNET_FORK
|
||||
? {
|
||||
blockNumber: 12012081,
|
||||
url: NETWORKS_RPC_URL['main'],
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const config: HardhatUserConfig = {
|
||||
solidity: {
|
||||
compilers: [
|
||||
{
|
||||
version: '0.8.10',
|
||||
settings: {
|
||||
optimizer: {
|
||||
enabled: true,
|
||||
runs: 200,
|
||||
details: {
|
||||
yul: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
networks: {
|
||||
kovan: getCommonNetworkConfig(eEthereumNetwork.kovan, 42),
|
||||
ropsten: getCommonNetworkConfig(eEthereumNetwork.ropsten, 3),
|
||||
main: getCommonNetworkConfig(eEthereumNetwork.main, 1),
|
||||
tenderlyMain: getCommonNetworkConfig(eEthereumNetwork.tenderlyMain, 3030),
|
||||
matic: getCommonNetworkConfig(ePolygonNetwork.matic, 137),
|
||||
mumbai: getCommonNetworkConfig(ePolygonNetwork.mumbai, 80001),
|
||||
xdai: getCommonNetworkConfig(eXDaiNetwork.xdai, 100),
|
||||
hardhat: {
|
||||
hardfork: 'london',
|
||||
blockGasLimit: DEFAULT_BLOCK_GAS_LIMIT,
|
||||
gas: DEFAULT_BLOCK_GAS_LIMIT,
|
||||
gasPrice: 8000000000,
|
||||
chainId: HARDHATEVM_CHAINID,
|
||||
throwOnTransactionFailures: true,
|
||||
throwOnCallFailures: true,
|
||||
accounts: accounts.map(({ secretKey, balance }: { secretKey: string; balance: string }) => ({
|
||||
privateKey: secretKey,
|
||||
balance,
|
||||
})),
|
||||
forking: mainnetFork,
|
||||
},
|
||||
},
|
||||
gasReporter: {
|
||||
enabled: TRACK_GAS,
|
||||
},
|
||||
spdxLicenseIdentifier: {
|
||||
overwrite: false,
|
||||
runOnCompile: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
45
helper-hardhat-config.ts
Normal file
45
helper-hardhat-config.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
eEthereumNetwork,
|
||||
ePolygonNetwork,
|
||||
eXDaiNetwork,
|
||||
iParamsPerNetwork,
|
||||
} from './helpers/types';
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config({});
|
||||
|
||||
const INFURA_KEY = process.env.INFURA_KEY || '';
|
||||
const ALCHEMY_KEY = process.env.ALCHEMY_KEY || '';
|
||||
const TENDERLY_FORK_ID = process.env.TENDERLY_FORK_ID || '';
|
||||
|
||||
const GWEI = 1000 * 1000 * 1000;
|
||||
|
||||
export const NETWORKS_RPC_URL: iParamsPerNetwork<string> = {
|
||||
[eEthereumNetwork.kovan]: ALCHEMY_KEY
|
||||
? `https://eth-kovan.alchemyapi.io/v2/${ALCHEMY_KEY}`
|
||||
: `https://kovan.infura.io/v3/${INFURA_KEY}`,
|
||||
[eEthereumNetwork.ropsten]: ALCHEMY_KEY
|
||||
? `https://eth-ropsten.alchemyapi.io/v2/${ALCHEMY_KEY}`
|
||||
: `https://ropsten.infura.io/v3/${INFURA_KEY}`,
|
||||
[eEthereumNetwork.main]: ALCHEMY_KEY
|
||||
? `https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_KEY}`
|
||||
: `https://mainnet.infura.io/v3/${INFURA_KEY}`,
|
||||
[eEthereumNetwork.hardhat]: 'http://localhost:8545',
|
||||
[eEthereumNetwork.harhatevm]: 'http://localhost:8545',
|
||||
[eEthereumNetwork.tenderlyMain]: `https://rpc.tenderly.co/fork/${TENDERLY_FORK_ID}`,
|
||||
[ePolygonNetwork.mumbai]: 'https://rpc-mumbai.maticvigil.com',
|
||||
[ePolygonNetwork.matic]: `https://polygon-mumbai.infura.io/v3/${INFURA_KEY}`,
|
||||
[eXDaiNetwork.xdai]: 'https://rpc.xdaichain.com/',
|
||||
};
|
||||
|
||||
export const NETWORKS_DEFAULT_GAS: iParamsPerNetwork<number> = {
|
||||
[eEthereumNetwork.kovan]: 65 * GWEI,
|
||||
[eEthereumNetwork.ropsten]: 65 * GWEI,
|
||||
[eEthereumNetwork.main]: 65 * GWEI,
|
||||
[eEthereumNetwork.hardhat]: 65 * GWEI,
|
||||
[eEthereumNetwork.harhatevm]: 65 * GWEI,
|
||||
[eEthereumNetwork.tenderlyMain]: 0.01 * GWEI,
|
||||
[ePolygonNetwork.mumbai]: 1 * GWEI,
|
||||
[ePolygonNetwork.matic]: 2 * GWEI,
|
||||
[eXDaiNetwork.xdai]: 1 * GWEI,
|
||||
};
|
||||
2
helpers/hardhat-constants.ts
Normal file
2
helpers/hardhat-constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const TEST_SNAPSHOT_ID = '0x1';
|
||||
export const HARDHATEVM_CHAINID = 31337;
|
||||
36
helpers/test-wallets.ts
Normal file
36
helpers/test-wallets.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
const balance = '1000000000000000000000000';
|
||||
|
||||
export const accounts = [
|
||||
{
|
||||
secretKey: '0xc5e8f61d1ab959b397eecc0a37a6517b8e67a0e7cf1f4bce5591f3ed80199122',
|
||||
balance,
|
||||
},
|
||||
{
|
||||
secretKey: '0xd49743deccbccc5dc7baa8e69e5be03298da8688a15dd202e20f15d5e0e9a9fb',
|
||||
balance,
|
||||
},
|
||||
{
|
||||
secretKey: '0x23c601ae397441f3ef6f1075dcb0031ff17fb079837beadaf3c84d96c6f3e569',
|
||||
balance,
|
||||
},
|
||||
{
|
||||
secretKey: '0xee9d129c1997549ee09c0757af5939b2483d80ad649a0eda68e8b0357ad11131',
|
||||
balance,
|
||||
},
|
||||
{
|
||||
secretKey: '0x87630b2d1de0fbd5044eb6891b3d9d98c34c8d310c852f98550ba774480e47cc',
|
||||
balance,
|
||||
},
|
||||
{
|
||||
secretKey: '0x275cc4a2bfd4f612625204a20a2280ab53a6da2d14860c47a9f5affe58ad86d4',
|
||||
balance,
|
||||
},
|
||||
{
|
||||
secretKey: '0xaee25d55ce586148a853ca83fdfacaf7bc42d5762c6e7187e6f8e822d8e6a650',
|
||||
balance,
|
||||
},
|
||||
{
|
||||
secretKey: '0xa2e0097c961c67ec197b6865d7ecea6caffc68ebeb00e6050368c8f67fc9c588',
|
||||
balance,
|
||||
},
|
||||
];
|
||||
68
helpers/types.ts
Normal file
68
helpers/types.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
export interface SymbolMap<T> {
|
||||
[symbol: string]: T;
|
||||
}
|
||||
|
||||
export type eNetwork = eEthereumNetwork | ePolygonNetwork | eXDaiNetwork;
|
||||
|
||||
export enum eEthereumNetwork {
|
||||
kovan = 'kovan',
|
||||
ropsten = 'ropsten',
|
||||
main = 'main',
|
||||
hardhat = 'hardhat',
|
||||
tenderlyMain = 'tenderlyMain',
|
||||
harhatevm = 'harhatevm',
|
||||
}
|
||||
|
||||
export enum ePolygonNetwork {
|
||||
matic = 'matic',
|
||||
mumbai = 'mumbai',
|
||||
}
|
||||
|
||||
export enum eXDaiNetwork {
|
||||
xdai = 'xdai',
|
||||
}
|
||||
|
||||
export enum EthereumNetworkNames {
|
||||
kovan = 'kovan',
|
||||
ropsten = 'ropsten',
|
||||
main = 'main',
|
||||
matic = 'matic',
|
||||
mumbai = 'mumbai',
|
||||
xdai = 'xdai',
|
||||
}
|
||||
|
||||
export type tEthereumAddress = string;
|
||||
export type tStringTokenBigUnits = string; // 1 ETH, or 10e6 USDC or 10e18 DAI
|
||||
export type tStringTokenSmallUnits = string; // 1 wei, or 1 basic unit of USDC, or 1 basic unit of DAI
|
||||
|
||||
export type iParamsPerNetwork<T> =
|
||||
| iEthereumParamsPerNetwork<T>
|
||||
| iPolygonParamsPerNetwork<T>
|
||||
| iXDaiParamsPerNetwork<T>;
|
||||
|
||||
export interface iParamsPerNetworkAll<T>
|
||||
extends iEthereumParamsPerNetwork<T>,
|
||||
iPolygonParamsPerNetwork<T>,
|
||||
iXDaiParamsPerNetwork<T> {}
|
||||
|
||||
export interface iEthereumParamsPerNetwork<eNetwork> {
|
||||
[eEthereumNetwork.harhatevm]: eNetwork;
|
||||
[eEthereumNetwork.kovan]: eNetwork;
|
||||
[eEthereumNetwork.ropsten]: eNetwork;
|
||||
[eEthereumNetwork.main]: eNetwork;
|
||||
[eEthereumNetwork.hardhat]: eNetwork;
|
||||
[eEthereumNetwork.tenderlyMain]: eNetwork;
|
||||
}
|
||||
|
||||
export interface iPolygonParamsPerNetwork<T> {
|
||||
[ePolygonNetwork.matic]: T;
|
||||
[ePolygonNetwork.mumbai]: T;
|
||||
}
|
||||
|
||||
export interface iXDaiParamsPerNetwork<T> {
|
||||
[eXDaiNetwork.xdai]: T;
|
||||
}
|
||||
|
||||
export interface ObjectString {
|
||||
[key: string]: string;
|
||||
}
|
||||
20
helpers/wallet-helpers.ts
Normal file
20
helpers/wallet-helpers.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Wallet, Signer } from 'ethers';
|
||||
import { DefenderRelaySigner, DefenderRelayProvider } from 'defender-relay-client/lib/ethers';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config({ path: '../.env' });
|
||||
|
||||
const PRIVATE_KEY = process.env.PRIVATE_KEY || '';
|
||||
const MNEMONIC = process.env.MNEMONIC || '';
|
||||
const DEFENDER_API_KEY = process.env.DEFENDER_API_KEY || '';
|
||||
const DEFENDER_SECRET_KEY = process.env.DEFENDER_SECRET_KEY || '';
|
||||
|
||||
export const getPrivateKeyWallet = (): Signer => new Wallet(PRIVATE_KEY);
|
||||
|
||||
export const getMnemonicWallet = (): Signer => Wallet.fromMnemonic(MNEMONIC);
|
||||
|
||||
export const getDefenderSigner = (): Signer => {
|
||||
const credentials = { apiKey: DEFENDER_API_KEY, apiSecret: DEFENDER_SECRET_KEY };
|
||||
const provider = new DefenderRelayProvider(credentials);
|
||||
return new DefenderRelaySigner(credentials, provider, { speed: 'fast' });
|
||||
};
|
||||
40867
package-lock.json
generated
Normal file
40867
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
87
package.json
Normal file
87
package.json
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"name": "lens-protocol",
|
||||
"version": "1.0.0",
|
||||
"description": "decentralized social media",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "npm run compile && TRACK_GAS=true hardhat test",
|
||||
"quick-test": "hardhat test",
|
||||
"spdx": "hardhat prepend-spdx-license",
|
||||
"size": "npm run compile && hardhat size-contracts",
|
||||
"full-deploy-local": "hardhat full-deploy --network localhost",
|
||||
"full-deploy-mumbai": "hardhat full-deploy --network mumbai",
|
||||
"coverage": "npm run compile && hardhat coverage --temp temp-artifacts --testfiles test/emptyrun.coverage.ts && hardhat coverage --temp temp-artifacts --testfiles '!test/emptyrun.coverage.ts'",
|
||||
"run-env": "npm i && tail -f /dev/null",
|
||||
"hardhat": "hardhat",
|
||||
"hardhat:kovan": "hardhat --network kovan",
|
||||
"hardhat:tenderly-main": "hardhat --network tenderlyMain",
|
||||
"hardhat:ropsten": "hardhat --network ropsten",
|
||||
"hardhat:main": "hardhat --network main",
|
||||
"hardhat:docker": "hardhat --network hardhatevm_docker",
|
||||
"hardhat:mumbai": "hardhat --network mumbai",
|
||||
"hardhat:matic": "hardhat --network matic",
|
||||
"compile": "SKIP_LOAD=true hardhat clean && SKIP_LOAD=true hardhat compile",
|
||||
"console:fork": "MAINNET_FORK=true hardhat console",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": ""
|
||||
},
|
||||
"bugs": {
|
||||
"url": ""
|
||||
},
|
||||
"homepage": "",
|
||||
"devDependencies": {
|
||||
"@nomiclabs/hardhat-ethers": "2.0.2",
|
||||
"@nomiclabs/hardhat-etherscan": "2.1.8",
|
||||
"@typechain/ethers-v5": "8.0.5",
|
||||
"@typechain/hardhat": "3.0.0",
|
||||
"@types/chai": "4.2.22",
|
||||
"@types/mocha": "9.0.0",
|
||||
"@types/node": "16.11.11",
|
||||
"@typescript-eslint/eslint-plugin": "5.5.0",
|
||||
"@typescript-eslint/parser": "5.5.0",
|
||||
"chai": "4.3.4",
|
||||
"defender-relay-client": "1.12.1",
|
||||
"dotenv": "10.0.0",
|
||||
"eslint": "8.3.0",
|
||||
"eslint-config-prettier": "8.3.0",
|
||||
"eslint-plugin-prettier": "4.0.0",
|
||||
"prettier-plugin-solidity": "1.0.0-beta.19",
|
||||
"ethereum-waffle": "3.4.0",
|
||||
"ethers": "5.5.1",
|
||||
"hardhat": "2.7.0",
|
||||
"hardhat-contract-sizer": "2.1.1",
|
||||
"hardhat-gas-reporter": "1.0.6",
|
||||
"hardhat-log-remover": "2.0.2",
|
||||
"hardhat-spdx-license-identifier": "2.0.3",
|
||||
"husky": "7.0.4",
|
||||
"prettier": "2.5.0",
|
||||
"solidity-coverage": "0.7.17",
|
||||
"ts-generator": "0.1.1",
|
||||
"ts-node": "10.4.0",
|
||||
"typechain": "6.0.5",
|
||||
"typescript": "4.5.2"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "pretty-quick --staged --pattern 'contracts/**/*.sol' --pattern 'helpers/**/*.ts' --pattern 'test/**/*.ts' --pattern 'tasks/**/*.ts'"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@openzeppelin/contracts": "4.4.0"
|
||||
},
|
||||
"author": "Lens",
|
||||
"contributors": [
|
||||
"Peter Michael (Zer0dot)",
|
||||
"Miguel Martinez",
|
||||
"Lasse Herskind",
|
||||
"Steven Valeri",
|
||||
"Alan Donoso Naumczuk",
|
||||
"Emilio Frangella",
|
||||
"David Racero"
|
||||
],
|
||||
"license": "AGPL-3.0-only"
|
||||
}
|
||||
195
tasks/full-deploy.ts
Normal file
195
tasks/full-deploy.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { hexlify, keccak256, RLP } from 'ethers/lib/utils';
|
||||
import fs from 'fs';
|
||||
import { task } from 'hardhat/config';
|
||||
import {
|
||||
LensHub__factory,
|
||||
ApprovalFollowModule__factory,
|
||||
CollectNFT__factory,
|
||||
Currency__factory,
|
||||
EmptyCollectModule__factory,
|
||||
FeeCollectModule__factory,
|
||||
FeeFollowModule__factory,
|
||||
FollowerOnlyReferenceModule__factory,
|
||||
FollowNFT__factory,
|
||||
InteractionLogic__factory,
|
||||
LimitedFeeCollectModule__factory,
|
||||
LimitedTimedFeeCollectModule__factory,
|
||||
ModuleGlobals__factory,
|
||||
PublishingLogic__factory,
|
||||
RevertCollectModule__factory,
|
||||
TimedFeeCollectModule__factory,
|
||||
TransparentUpgradeableProxy__factory,
|
||||
} from '../typechain-types';
|
||||
import { deployContract, waitForTx } from './helpers/utils';
|
||||
|
||||
const TREASURY_FEE_BPS = 50;
|
||||
const LENS_HUB_NFT_NAME = 'Various Vegetables';
|
||||
const LENS_HUB_NFT_SYMBOL = 'VVGT';
|
||||
|
||||
task('full-deploy', 'deploys the entire Lens protocol').setAction(async ({}, hre) => {
|
||||
// Note that the use of these signers is a placeholder and is not meant to be used in
|
||||
// production.
|
||||
const ethers = hre.ethers;
|
||||
const accounts = await ethers.getSigners();
|
||||
const deployer = accounts[0];
|
||||
const governance = accounts[1];
|
||||
const deployerAddress = deployer.address;
|
||||
const governanceAddress = governance.address;
|
||||
const treasuryAddress = accounts[2].address;
|
||||
|
||||
const moduleGlobals = await new ModuleGlobals__factory(deployer).deploy(
|
||||
governanceAddress,
|
||||
treasuryAddress,
|
||||
TREASURY_FEE_BPS
|
||||
);
|
||||
|
||||
console.log('\n\t-- Deploying Logic Libs --');
|
||||
|
||||
const publishingLogic = await deployContract(new PublishingLogic__factory(deployer).deploy());
|
||||
const interactionLogic = await deployContract(new InteractionLogic__factory(deployer).deploy());
|
||||
const hubLibs = {
|
||||
'contracts/libraries/PublishingLogic.sol:PublishingLogic': publishingLogic.address,
|
||||
'contracts/libraries/InteractionLogic.sol:InteractionLogic': interactionLogic.address,
|
||||
};
|
||||
|
||||
// Here, we pre-compute the nonces and addresses used to deploy the contracts.
|
||||
const nonce = await deployer.getTransactionCount();
|
||||
const followNFTNonce = hexlify(nonce + 1);
|
||||
const collectNFTNonce = hexlify(nonce + 2);
|
||||
const hubProxyNonce = hexlify(nonce + 3);
|
||||
|
||||
const followNFTImplAddress =
|
||||
'0x' + keccak256(RLP.encode([deployerAddress, followNFTNonce])).substr(26);
|
||||
const collectNFTImplAddress =
|
||||
'0x' + keccak256(RLP.encode([deployerAddress, collectNFTNonce])).substr(26);
|
||||
const hubProxyAddress = '0x' + keccak256(RLP.encode([deployerAddress, hubProxyNonce])).substr(26);
|
||||
|
||||
// Next, we deploy first the hub implementation, then the followNFT implementation, the collectNFT, and finally the
|
||||
// hub proxy with initialization.
|
||||
console.log('\n\t-- Deploying Hub Implementation --');
|
||||
|
||||
const lensHubImpl = await deployContract(
|
||||
new LensHub__factory(hubLibs, deployer).deploy(followNFTImplAddress, collectNFTImplAddress)
|
||||
);
|
||||
|
||||
console.log('\n\t-- Deploying Follow & Collect NFT Implementations --');
|
||||
await deployContract(new FollowNFT__factory(deployer).deploy(hubProxyAddress));
|
||||
await deployContract(new CollectNFT__factory(deployer).deploy(hubProxyAddress));
|
||||
|
||||
let data = lensHubImpl.interface.encodeFunctionData('initialize', [
|
||||
LENS_HUB_NFT_NAME,
|
||||
LENS_HUB_NFT_SYMBOL,
|
||||
governanceAddress,
|
||||
]);
|
||||
|
||||
console.log('\n\t-- Deploying Hub Proxy --');
|
||||
|
||||
let proxy = await deployContract(
|
||||
new TransparentUpgradeableProxy__factory(deployer).deploy(
|
||||
lensHubImpl.address,
|
||||
deployerAddress,
|
||||
data
|
||||
)
|
||||
);
|
||||
|
||||
// Connect the hub proxy to the LensHub factory and the governance for ease of use.
|
||||
const lensHub = LensHub__factory.connect(proxy.address, governance);
|
||||
|
||||
// Currency
|
||||
console.log('\n\t-- Deploying Currency --');
|
||||
const currency = await deployContract(new Currency__factory(deployer).deploy());
|
||||
|
||||
// Deploy collect modules
|
||||
console.log('\n\t-- Deploying feeCollectModule --');
|
||||
const feeCollectModule = await deployContract(
|
||||
new FeeCollectModule__factory(deployer).deploy(lensHub.address, moduleGlobals.address)
|
||||
);
|
||||
console.log('\n\t-- Deploying limitedFeeCollectModule --');
|
||||
const limitedFeeCollectModule = await deployContract(
|
||||
new LimitedFeeCollectModule__factory(deployer).deploy(lensHub.address, moduleGlobals.address)
|
||||
);
|
||||
console.log('\n\t-- Deploying timedFeeCollectModule --');
|
||||
const timedFeeCollectModule = await deployContract(
|
||||
new TimedFeeCollectModule__factory(deployer).deploy(lensHub.address, moduleGlobals.address)
|
||||
);
|
||||
console.log('\n\t-- Deploying limitedTimedFeeCollectModule --');
|
||||
const limitedTimedFeeCollectModule = await deployContract(
|
||||
new LimitedTimedFeeCollectModule__factory(deployer).deploy(
|
||||
lensHub.address,
|
||||
moduleGlobals.address
|
||||
)
|
||||
);
|
||||
|
||||
console.log('\n\t-- Deploying revertCollectModule --');
|
||||
const revertCollectModule = await deployContract(
|
||||
new RevertCollectModule__factory(deployer).deploy()
|
||||
);
|
||||
console.log('\n\t-- Deploying emptyCollectModule --');
|
||||
const emptyCollectModule = await deployContract(
|
||||
new EmptyCollectModule__factory(deployer).deploy(lensHub.address)
|
||||
);
|
||||
|
||||
// Deploy follow modules
|
||||
console.log('\n\t-- Deploying feeFollowModule --');
|
||||
const feeFollowModule = await deployContract(
|
||||
new FeeFollowModule__factory(deployer).deploy(lensHub.address, moduleGlobals.address)
|
||||
);
|
||||
console.log('\n\t-- Deploying approvalFollowModule --');
|
||||
const approvalFollowModule = await deployContract(
|
||||
new ApprovalFollowModule__factory(deployer).deploy(lensHub.address)
|
||||
);
|
||||
|
||||
// Deploy reference module
|
||||
console.log('\n\t-- Deploying followerOnlyReferenceModule --');
|
||||
const followerOnlyReferenceModule = await deployContract(
|
||||
new FollowerOnlyReferenceModule__factory(deployer).deploy(lensHub.address)
|
||||
);
|
||||
|
||||
// Whitelist the collect modules
|
||||
console.log('\n\t-- Whitelisting Collect Modules --');
|
||||
await waitForTx(lensHub.whitelistCollectModule(feeCollectModule.address, true));
|
||||
await waitForTx(lensHub.whitelistCollectModule(limitedFeeCollectModule.address, true));
|
||||
await waitForTx(lensHub.whitelistCollectModule(timedFeeCollectModule.address, true));
|
||||
await waitForTx(lensHub.whitelistCollectModule(limitedTimedFeeCollectModule.address, true));
|
||||
await waitForTx(lensHub.whitelistCollectModule(revertCollectModule.address, true));
|
||||
await waitForTx(lensHub.whitelistCollectModule(emptyCollectModule.address, true));
|
||||
|
||||
// Whitelist the follow modules
|
||||
console.log('\n\t-- Whitelisting Follow Modules --');
|
||||
await waitForTx(lensHub.whitelistFollowModule(feeFollowModule.address, true));
|
||||
await waitForTx(lensHub.whitelistFollowModule(approvalFollowModule.address, true));
|
||||
|
||||
// Whitelist the reference module
|
||||
console.log('\n\t-- Whitelisting Reference Module --');
|
||||
await waitForTx(lensHub.whitelistReferenceModule(followerOnlyReferenceModule.address, true));
|
||||
|
||||
// Whitelist the currency
|
||||
console.log('\n\t-- Whitelisting Currency in Module Globals --');
|
||||
await waitForTx(moduleGlobals.connect(governance).whitelistCurrency(currency.address, true));
|
||||
|
||||
// Save and log the addresses
|
||||
const addrs = {
|
||||
'lensHub proxy': lensHub.address,
|
||||
'lensHub impl:': lensHubImpl.address,
|
||||
'publishing logic lib': publishingLogic.address,
|
||||
'interaction logic lib': interactionLogic.address,
|
||||
'follow NFT impl': followNFTImplAddress,
|
||||
'collect NFT impl': collectNFTImplAddress,
|
||||
currency: currency.address,
|
||||
'module globals': moduleGlobals.address,
|
||||
'fee collect module': feeCollectModule.address,
|
||||
'limited fee collect module': limitedFeeCollectModule.address,
|
||||
'timed fee collect module': timedFeeCollectModule.address,
|
||||
'limited timed fee collect module': limitedTimedFeeCollectModule.address,
|
||||
'revert collect module': revertCollectModule.address,
|
||||
'empty collect module': emptyCollectModule.address,
|
||||
'fee follow module': feeFollowModule.address,
|
||||
'approval follow module': approvalFollowModule.address,
|
||||
'follower only reference module': followerOnlyReferenceModule.address,
|
||||
};
|
||||
const json = JSON.stringify(addrs, null, 2);
|
||||
console.log(json);
|
||||
|
||||
fs.writeFileSync('addresses.json', json, 'utf-8');
|
||||
});
|
||||
41
tasks/helpers/utils.ts
Normal file
41
tasks/helpers/utils.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { BytesLike, Signer, Wallet, ContractTransaction, BaseContract } from 'ethers';
|
||||
import { HardhatRuntimeEnvironment } from 'hardhat/types';
|
||||
import { LensHub__factory } from '../../typechain-types';
|
||||
import fs from 'fs';
|
||||
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
|
||||
|
||||
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
||||
|
||||
export enum ProtocolState {
|
||||
Unpaused,
|
||||
PublishingPaused,
|
||||
Paused,
|
||||
}
|
||||
|
||||
export function getAddrs(): any {
|
||||
const json = fs.readFileSync('addresses.json', 'utf8');
|
||||
const addrs = JSON.parse(json);
|
||||
return addrs;
|
||||
}
|
||||
|
||||
export async function waitForTx(tx: Promise<ContractTransaction>) {
|
||||
await (await tx).wait();
|
||||
}
|
||||
|
||||
export async function deployContract(tx: any): Promise<any> {
|
||||
const result = await tx;
|
||||
await result.deployTransaction.wait();
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function initEnv(hre: HardhatRuntimeEnvironment): Promise<SignerWithAddress[]> {
|
||||
const ethers = hre.ethers; // This allows us to access the hre (Hardhat runtime environment)'s injected ethers instance easily
|
||||
|
||||
const accounts = await ethers.getSigners(); // This returns an array of the default signers connected to the hre's ethers instance
|
||||
const governance = accounts[1];
|
||||
const treasury = accounts[2];
|
||||
const user = accounts[3];
|
||||
|
||||
return [governance, treasury, user];
|
||||
}
|
||||
18
tasks/whitelist-currency.ts
Normal file
18
tasks/whitelist-currency.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { task } from 'hardhat/config';
|
||||
import { ModuleGlobals__factory } from '../typechain-types';
|
||||
import { waitForTx } from './helpers/utils';
|
||||
|
||||
task('whitelist-currency', 'whitelists a currency in the module globals')
|
||||
.addParam('gov')
|
||||
.addParam('globals')
|
||||
.addParam('currency')
|
||||
.addParam('whitelist')
|
||||
.setAction(async ({ gov, globals, currency, whitelist }, hre) => {
|
||||
const ethers = hre.ethers;
|
||||
const governance = await ethers.getSigner(gov);
|
||||
|
||||
const moduleGlobals = ModuleGlobals__factory.connect(globals, governance);
|
||||
|
||||
await waitForTx(moduleGlobals.connect(governance).whitelistCurrency(currency, whitelist));
|
||||
});
|
||||
242
test/__setup.spec.ts
Normal file
242
test/__setup.spec.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
import { AbiCoder } from '@ethersproject/contracts/node_modules/@ethersproject/abi';
|
||||
import { parseEther } from '@ethersproject/units';
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect, use } from 'chai';
|
||||
import { solidity } from 'ethereum-waffle';
|
||||
import { BytesLike, Signer, Wallet } from 'ethers';
|
||||
import { ethers } from 'hardhat';
|
||||
import {
|
||||
ApprovalFollowModule,
|
||||
ApprovalFollowModule__factory,
|
||||
CollectNFT__factory,
|
||||
Currency,
|
||||
Currency__factory,
|
||||
EmptyCollectModule,
|
||||
EmptyCollectModule__factory,
|
||||
Events,
|
||||
Events__factory,
|
||||
FeeCollectModule,
|
||||
FeeCollectModule__factory,
|
||||
FeeFollowModule,
|
||||
FeeFollowModule__factory,
|
||||
FollowerOnlyReferenceModule,
|
||||
FollowerOnlyReferenceModule__factory,
|
||||
FollowNFT__factory,
|
||||
Helper,
|
||||
Helper__factory,
|
||||
InteractionLogic__factory,
|
||||
LensHub,
|
||||
LensHub__factory,
|
||||
LimitedFeeCollectModule,
|
||||
LimitedFeeCollectModule__factory,
|
||||
LimitedTimedFeeCollectModule,
|
||||
LimitedTimedFeeCollectModule__factory,
|
||||
MockFollowModule,
|
||||
MockFollowModule__factory,
|
||||
MockReferenceModule,
|
||||
MockReferenceModule__factory,
|
||||
ModuleGlobals,
|
||||
ModuleGlobals__factory,
|
||||
PublishingLogic__factory,
|
||||
RevertCollectModule,
|
||||
RevertCollectModule__factory,
|
||||
TimedFeeCollectModule,
|
||||
TimedFeeCollectModule__factory,
|
||||
TransparentUpgradeableProxy__factory,
|
||||
} from '../typechain-types';
|
||||
import { LensHubLibraryAddresses } from '../typechain-types/factories/LensHub__factory';
|
||||
import { FAKE_PRIVATEKEY, ZERO_ADDRESS } from './helpers/constants';
|
||||
import {
|
||||
computeContractAddress,
|
||||
ProtocolState,
|
||||
revertToSnapshot,
|
||||
takeSnapshot,
|
||||
} from './helpers/utils';
|
||||
|
||||
use(solidity);
|
||||
|
||||
export const CURRENCY_MINT_AMOUNT = parseEther('100');
|
||||
export const BPS_MAX = 10000;
|
||||
export const TREASURY_FEE_BPS = 50;
|
||||
export const REFERRAL_FEE_BPS = 250;
|
||||
export const LENS_HUB_NFT_NAME = 'Lens Profiles';
|
||||
export const LENS_HUB_NFT_SYMBOL = 'LENS';
|
||||
export const MOCK_PROFILE_HANDLE = 'plant1ghost.eth';
|
||||
export const FIRST_PROFILE_ID = 1;
|
||||
export const MOCK_URI =
|
||||
'https://ipfs.fleek.co/ipfs/plantghostplantghostplantghostplantghostplantghostplantghos';
|
||||
export const OTHER_MOCK_URI =
|
||||
'https://ipfs.fleek.co/ipfs/ghostplantghostplantghostplantghostplantghostplantghostplan';
|
||||
export const MOCK_PROFILE_URI =
|
||||
'https://ipfs.fleek.co/ipfs/runningoutofthingstowriterunningoutofthingstowriterunningou';
|
||||
export const MOCK_FOLLOW_NFT_URI =
|
||||
'https://ipfs.fleek.co/ipfs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
||||
|
||||
export let accounts: Signer[];
|
||||
export let deployer: Signer;
|
||||
export let user: Signer;
|
||||
export let userTwo: Signer;
|
||||
export let governance: Signer;
|
||||
export let deployerAddress: string;
|
||||
export let userAddress: string;
|
||||
export let userTwoAddress: string;
|
||||
export let governanceAddress: string;
|
||||
export let followNFTImplAddress: string;
|
||||
export let collectNFTImplAddress: string;
|
||||
export let treasuryAddress: string;
|
||||
export let testWallet: Wallet;
|
||||
export let lensHubImpl: LensHub;
|
||||
export let lensHub: LensHub;
|
||||
export let currency: Currency;
|
||||
export let abiCoder: AbiCoder;
|
||||
export let mockModuleData: BytesLike;
|
||||
export let hubLibs: LensHubLibraryAddresses;
|
||||
export let eventsLib: Events;
|
||||
export let moduleGlobals: ModuleGlobals;
|
||||
export let helper: Helper;
|
||||
|
||||
/* Modules */
|
||||
|
||||
// Collect
|
||||
export let feeCollectModule: FeeCollectModule;
|
||||
export let timedFeeCollectModule: TimedFeeCollectModule;
|
||||
export let emptyCollectModule: EmptyCollectModule;
|
||||
export let revertCollectModule: RevertCollectModule;
|
||||
export let limitedFeeCollectModule: LimitedFeeCollectModule;
|
||||
export let limitedTimedFeeCollectModule: LimitedTimedFeeCollectModule;
|
||||
|
||||
// Follow
|
||||
export let approvalFollowModule: ApprovalFollowModule;
|
||||
export let feeFollowModule: FeeFollowModule;
|
||||
export let mockFollowModule: MockFollowModule;
|
||||
|
||||
// Reference
|
||||
export let followerOnlyReferenceModule: FollowerOnlyReferenceModule;
|
||||
export let mockReferenceModule: MockReferenceModule;
|
||||
|
||||
export function makeSuiteCleanRoom(name: string, tests: () => void) {
|
||||
describe(name, () => {
|
||||
beforeEach(async function () {
|
||||
await takeSnapshot();
|
||||
});
|
||||
tests();
|
||||
afterEach(async function () {
|
||||
await revertToSnapshot();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
abiCoder = ethers.utils.defaultAbiCoder;
|
||||
testWallet = new ethers.Wallet(FAKE_PRIVATEKEY).connect(ethers.provider);
|
||||
accounts = await ethers.getSigners();
|
||||
deployer = accounts[0];
|
||||
user = accounts[1];
|
||||
userTwo = accounts[2];
|
||||
governance = accounts[3];
|
||||
deployerAddress = await deployer.getAddress();
|
||||
userAddress = await user.getAddress();
|
||||
userTwoAddress = await userTwo.getAddress();
|
||||
governanceAddress = await governance.getAddress();
|
||||
treasuryAddress = await accounts[4].getAddress();
|
||||
mockModuleData = abiCoder.encode(['uint256'], [1]);
|
||||
// Deployment
|
||||
helper = await new Helper__factory(deployer).deploy();
|
||||
moduleGlobals = await new ModuleGlobals__factory(deployer).deploy(
|
||||
governanceAddress,
|
||||
treasuryAddress,
|
||||
TREASURY_FEE_BPS
|
||||
);
|
||||
const publishingLogic = await new PublishingLogic__factory(deployer).deploy();
|
||||
const interactionLogic = await new InteractionLogic__factory(deployer).deploy();
|
||||
hubLibs = {
|
||||
'contracts/libraries/PublishingLogic.sol:PublishingLogic': publishingLogic.address,
|
||||
'contracts/libraries/InteractionLogic.sol:InteractionLogic': interactionLogic.address,
|
||||
};
|
||||
|
||||
// Here, we pre-compute the nonces and addresses used to deploy the contracts.
|
||||
const nonce = await deployer.getTransactionCount();
|
||||
// nonce + 0 is follow NFT impl
|
||||
// nonce + 1 is collect NFT impl
|
||||
// nonce + 2 is impl
|
||||
// nonce + 3 is hub proxy
|
||||
|
||||
const hubProxyAddress = computeContractAddress(deployerAddress, nonce + 3); //'0x' + keccak256(RLP.encode([deployerAddress, hubProxyNonce])).substr(26);
|
||||
|
||||
const followNFTImpl = await new FollowNFT__factory(deployer).deploy(hubProxyAddress);
|
||||
const collectNFTImpl = await new CollectNFT__factory(deployer).deploy(hubProxyAddress);
|
||||
|
||||
lensHubImpl = await new LensHub__factory(hubLibs, deployer).deploy(
|
||||
followNFTImpl.address,
|
||||
collectNFTImpl.address
|
||||
);
|
||||
|
||||
let data = lensHubImpl.interface.encodeFunctionData('initialize', [
|
||||
LENS_HUB_NFT_NAME,
|
||||
LENS_HUB_NFT_SYMBOL,
|
||||
governanceAddress,
|
||||
]);
|
||||
let proxy = await new TransparentUpgradeableProxy__factory(deployer).deploy(
|
||||
lensHubImpl.address,
|
||||
deployerAddress,
|
||||
data
|
||||
);
|
||||
|
||||
// Connect the hub proxy to the LensHub factory and the user for ease of use.
|
||||
lensHub = LensHub__factory.connect(proxy.address, user);
|
||||
|
||||
// Currency
|
||||
currency = await new Currency__factory(deployer).deploy();
|
||||
|
||||
// Modules
|
||||
emptyCollectModule = await new EmptyCollectModule__factory(deployer).deploy(lensHub.address);
|
||||
revertCollectModule = await new RevertCollectModule__factory(deployer).deploy();
|
||||
feeCollectModule = await new FeeCollectModule__factory(deployer).deploy(
|
||||
lensHub.address,
|
||||
moduleGlobals.address
|
||||
);
|
||||
timedFeeCollectModule = await new TimedFeeCollectModule__factory(deployer).deploy(
|
||||
lensHub.address,
|
||||
moduleGlobals.address
|
||||
);
|
||||
limitedFeeCollectModule = await new LimitedFeeCollectModule__factory(deployer).deploy(
|
||||
lensHub.address,
|
||||
moduleGlobals.address
|
||||
);
|
||||
limitedTimedFeeCollectModule = await new LimitedTimedFeeCollectModule__factory(deployer).deploy(
|
||||
lensHub.address,
|
||||
moduleGlobals.address
|
||||
);
|
||||
|
||||
feeFollowModule = await new FeeFollowModule__factory(deployer).deploy(
|
||||
lensHub.address,
|
||||
moduleGlobals.address
|
||||
);
|
||||
approvalFollowModule = await new ApprovalFollowModule__factory(deployer).deploy(lensHub.address);
|
||||
followerOnlyReferenceModule = await new FollowerOnlyReferenceModule__factory(deployer).deploy(
|
||||
lensHub.address
|
||||
);
|
||||
|
||||
mockFollowModule = await new MockFollowModule__factory(deployer).deploy();
|
||||
mockReferenceModule = await new MockReferenceModule__factory(deployer).deploy();
|
||||
|
||||
await expect(lensHub.connect(governance).setState(ProtocolState.Unpaused)).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistProfileCreator(userAddress, true)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistProfileCreator(userTwoAddress, true)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistProfileCreator(testWallet.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
expect(lensHub).to.not.be.undefined;
|
||||
expect(currency).to.not.be.undefined;
|
||||
expect(timedFeeCollectModule).to.not.be.undefined;
|
||||
expect(mockFollowModule).to.not.be.undefined;
|
||||
expect(mockReferenceModule).to.not.be.undefined;
|
||||
|
||||
// Event library deployment is only needed for testing and is not reproduced in the live environment
|
||||
eventsLib = await new Events__factory(deployer).deploy();
|
||||
});
|
||||
3
test/emptyrun.coverage.ts
Normal file
3
test/emptyrun.coverage.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { makeSuiteCleanRoom } from './__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Empty Run for Coverage', function () {});
|
||||
8
test/helpers/constants.ts
Normal file
8
test/helpers/constants.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const MAX_UINT256 = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
|
||||
export const WETH_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
|
||||
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
||||
|
||||
// Fetched from $npx hardhat node, account # 7
|
||||
export const FAKE_PRIVATEKEY = '0xa2e0097c961c67ec197b6865d7ecea6caffc68ebeb00e6050368c8f67fc9c588';
|
||||
|
||||
export const HARDHAT_CHAINID = 31337;
|
||||
45
test/helpers/errors.ts
Normal file
45
test/helpers/errors.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export const ERRORS = {
|
||||
CANNOT_INIT_IMPL: 'CannotInitImplementation()',
|
||||
INITIALIZED: 'Initialized()',
|
||||
SIGNATURE_EXPIRED: 'SignatureExpired()',
|
||||
ZERO_SPENDER: 'ZeroSpender()',
|
||||
SIGNATURE_INVALID: 'SignatureInvalid()',
|
||||
NOT_OWNER_OR_APPROVED: 'NotOwnerOrApproved()',
|
||||
NOT_HUB: 'NotHub()',
|
||||
TOKEN_DOES_NOT_EXIST: 'TokenDoesNotExist()',
|
||||
CALLER_NOT_WHITELSITED_MODULE: 'CallerNotWhitelistedModule()',
|
||||
NOT_GOVERNANCE: 'NotGovernance()',
|
||||
COLLECT_MODULE_NOT_WHITELISTED: 'CollectModuleNotWhitelisted()',
|
||||
FOLLOW_MODULE_NOT_WHITELISTED: 'FollowModuleNotWhitelisted()',
|
||||
REFERENCE_MODULE_NOT_WHITELISTED: 'ReferenceModuleNotWhitelisted()',
|
||||
PROFILE_CREATOR_NOT_WHITELISTED: 'ProfileCreatorNotWhitelisted()',
|
||||
NOT_PROFILE_OWNER: 'NotProfileOwner()',
|
||||
NOT_PROFILE_OWNER_OR_DISPATCHER: 'NotProfileOwnerOrDispatcher()',
|
||||
PUBLICATION_DOES_NOT_EXIST: 'PublicationDoesNotExist()',
|
||||
PROFILE_HANDLE_TAKEN: 'HandleTaken()',
|
||||
INVALID_HANDLE_LENGTH: 'HandleLengthInvalid()',
|
||||
HANDLE_CONTAINS_INVALID_CHARACTERS: 'HandleContainsInvalidCharacters()',
|
||||
NOT_FOLLOW_NFT: 'CallerNotFollowNFT()',
|
||||
NOT_COLLECT_NFT: 'CallerNotCollectNFT()',
|
||||
BLOCK_NUMBER_INVALID: 'BlockNumberInvalid()',
|
||||
INIT_PARAMS_INVALID: 'InitParamsInvalid()',
|
||||
ZERO_CURRENCY: 'ZeroCurrency()',
|
||||
COLLECT_EXPIRED: 'CollectExpired()',
|
||||
COLLECT_NOT_ALLOWED: 'CollectNotAllowed()',
|
||||
MINT_LIMIT_EXCEEDED: 'MintLimitExceeded()',
|
||||
FOLLOW_INVALID: 'FollowInvalid()',
|
||||
MODULE_DATA_MISMATCH: 'ModuleDataMismatch()',
|
||||
FOLLOW_NOT_APPROVED: 'FollowNotApproved()',
|
||||
ARRAY_MISMATCH: 'ArrayMismatch()',
|
||||
ERC721_NOT_OWN: 'ERC721: transfer of token that is not own',
|
||||
ERC721_TRANSFER_NOT_OWNER_OR_APPROVED: 'ERC721: transfer caller is not owner nor approved',
|
||||
ERC721_QUERY_FOR_NONEXISTENT_TOKEN: 'ERC721: owner query for nonexistent token',
|
||||
ERC20_TRANSFER_EXCEEDS_ALLOWANCE: 'ERC20: transfer amount exceeds allowance',
|
||||
NO_SELECTOR:
|
||||
"Transaction reverted: function selector was not recognized and there's no fallback function",
|
||||
PAUSED: 'Paused()',
|
||||
PUBLISHING_PAUSED: 'PublishingPaused()',
|
||||
NOT_GOVERNANCE_OR_EMERGENCY_ADMIN: 'NotGovernanceOrEmergencyAdmin()',
|
||||
NO_REASON_ABI_DECODE:
|
||||
"Transaction reverted and Hardhat couldn't infer the reason. Please report this to help us improve Hardhat.",
|
||||
};
|
||||
791
test/helpers/utils.ts
Normal file
791
test/helpers/utils.ts
Normal file
@@ -0,0 +1,791 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import {
|
||||
BigNumberish,
|
||||
Bytes,
|
||||
Event,
|
||||
logger,
|
||||
utils,
|
||||
BigNumber,
|
||||
Contract,
|
||||
ContractReceipt,
|
||||
} from 'ethers';
|
||||
import { TransactionReceipt } from '@ethersproject/providers';
|
||||
import { hexlify, keccak256, RLP, toUtf8Bytes } from 'ethers/lib/utils';
|
||||
import { TransactionResponse } from '@ethersproject/providers';
|
||||
import hre from 'hardhat';
|
||||
import { LensHub__factory } from '../../typechain-types';
|
||||
import { lensHub, LENS_HUB_NFT_NAME, helper, testWallet, eventsLib } from '../__setup.spec';
|
||||
import { HARDHAT_CHAINID, MAX_UINT256 } from './constants';
|
||||
import { expect } from 'chai';
|
||||
|
||||
export enum ProtocolState {
|
||||
Unpaused,
|
||||
PublishingPaused,
|
||||
Paused,
|
||||
}
|
||||
|
||||
export function matchEvent(
|
||||
receipt: TransactionReceipt,
|
||||
name: string,
|
||||
expectedArgs?: any[],
|
||||
eventContract: Contract = eventsLib
|
||||
) {
|
||||
const events = receipt.logs;
|
||||
|
||||
if (events != undefined) {
|
||||
// match name from list of events in eventContract, when found, compute the sigHash
|
||||
let sigHash: string | undefined;
|
||||
for (let contractEvents of Object.keys(eventContract.interface.events)) {
|
||||
if (contractEvents.startsWith(name) && contractEvents.charAt(name.length) == '(') {
|
||||
sigHash = keccak256(toUtf8Bytes(contractEvents));
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Throw if the sigHash was not found
|
||||
if (!sigHash) {
|
||||
logger.throwError(
|
||||
`Event "${name}" not found in provided contract (default: Events libary). \nAre you sure you're using the right contract?`
|
||||
);
|
||||
}
|
||||
|
||||
// Find the given event in the emitted logs
|
||||
let invalidParamsButExists = false;
|
||||
for (let emittedEvent of events) {
|
||||
// If we find one with the correct sighash, check if it is the one we're looking for
|
||||
if (emittedEvent.topics[0] == sigHash) {
|
||||
const event = eventContract.interface.parseLog(emittedEvent);
|
||||
// If there are expected arguments, validate them, otherwise, return here
|
||||
if (expectedArgs) {
|
||||
if (expectedArgs.length != event.args.length) {
|
||||
logger.throwError(
|
||||
`Event "${name}" emitted with correct signature, but expected args are of invalid length`
|
||||
);
|
||||
}
|
||||
invalidParamsButExists = false;
|
||||
// Iterate through arguments and check them, if there is a mismatch, continue with the loop
|
||||
for (let i = 0; i < expectedArgs.length; i++) {
|
||||
// Parse empty arrays as empty bytes
|
||||
if (expectedArgs[i].constructor == Array && expectedArgs[i].length == 0) {
|
||||
expectedArgs[i] = '0x';
|
||||
}
|
||||
|
||||
// Break out of the expected args loop if there is a mismatch, this will continue the emitted event loop
|
||||
if (BigNumber.isBigNumber(event.args[i])) {
|
||||
if (!event.args[i].eq(BigNumber.from(expectedArgs[i]))) {
|
||||
invalidParamsButExists = true;
|
||||
break;
|
||||
}
|
||||
} else if (event.args[i].constructor == Array) {
|
||||
let params = event.args[i];
|
||||
let expected = expectedArgs[i];
|
||||
for (let j = 0; j < params.length; j++) {
|
||||
if (BigNumber.isBigNumber(params[j])) {
|
||||
if (!params[j].eq(BigNumber.from(expected[j]))) {
|
||||
invalidParamsButExists = true;
|
||||
break;
|
||||
}
|
||||
} else if (params[j] != expected[j]) {
|
||||
invalidParamsButExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (invalidParamsButExists) break;
|
||||
} else if (event.args[i] != expectedArgs[i]) {
|
||||
invalidParamsButExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Return if the for loop did not cause a break, so a match has been found, otherwise proceed with the event loop
|
||||
if (!invalidParamsButExists) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Throw if the event args were not expected or the event was not found in the logs
|
||||
if (invalidParamsButExists) {
|
||||
logger.throwError(`Event "${name}" found in logs but with unexpected args`);
|
||||
} else {
|
||||
logger.throwError(`Event "${name}" not found in given transaction log`);
|
||||
}
|
||||
} else {
|
||||
logger.throwError('No events were emitted');
|
||||
}
|
||||
}
|
||||
|
||||
export function computeContractAddress(deployerAddress: string, nonce: number): string {
|
||||
const hexNonce = hexlify(nonce);
|
||||
return '0x' + keccak256(RLP.encode([deployerAddress, hexNonce])).substr(26);
|
||||
}
|
||||
|
||||
export function getChainId(): number {
|
||||
return hre.network.config.chainId || HARDHAT_CHAINID;
|
||||
}
|
||||
|
||||
export function getAbbreviation(handle: string) {
|
||||
let slice = handle.substr(0, 4);
|
||||
if (slice.charAt(3) == ' ') {
|
||||
slice = slice.substr(0, 3);
|
||||
}
|
||||
return slice;
|
||||
}
|
||||
|
||||
export async function waitForTx(
|
||||
tx: Promise<TransactionResponse> | TransactionResponse,
|
||||
skipCheck = false
|
||||
): Promise<TransactionReceipt> {
|
||||
if (!skipCheck) await expect(tx).to.not.be.reverted;
|
||||
return await (await tx).wait();
|
||||
}
|
||||
|
||||
export async function getBlockNumber(): Promise<number> {
|
||||
return (await helper.getBlockNumber()).toNumber();
|
||||
}
|
||||
|
||||
export async function resetFork(): Promise<void> {
|
||||
await hre.network.provider.request({
|
||||
method: 'hardhat_reset',
|
||||
params: [
|
||||
{
|
||||
forking: {
|
||||
jsonRpcUrl: `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_KEY}`,
|
||||
blockNumber: 12012081,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
console.log('\t> Fork reset');
|
||||
|
||||
await hre.network.provider.request({
|
||||
method: 'evm_setNextBlockTimestamp',
|
||||
params: [1614290545], // Original block timestamp + 1
|
||||
});
|
||||
|
||||
console.log('\t> Timestamp reset to 1614290545');
|
||||
}
|
||||
|
||||
export async function getTimestamp(): Promise<any> {
|
||||
const blockNumber = await hre.ethers.provider.send('eth_blockNumber', []);
|
||||
const block = await hre.ethers.provider.send('eth_getBlockByNumber', [blockNumber, false]);
|
||||
return block.timestamp;
|
||||
}
|
||||
|
||||
export async function setNextBlockTimestamp(timestamp: number): Promise<void> {
|
||||
await hre.ethers.provider.send('evm_setNextBlockTimestamp', [timestamp]);
|
||||
}
|
||||
|
||||
export async function mine(blocks: number): Promise<void> {
|
||||
for (let i = 0; i < blocks; i++) {
|
||||
await hre.ethers.provider.send('evm_mine', []);
|
||||
}
|
||||
}
|
||||
|
||||
let snapshotId: string = '0x1';
|
||||
export async function takeSnapshot() {
|
||||
snapshotId = await hre.ethers.provider.send('evm_snapshot', []);
|
||||
}
|
||||
|
||||
export async function revertToSnapshot() {
|
||||
await hre.ethers.provider.send('evm_revert', [snapshotId]);
|
||||
}
|
||||
|
||||
export async function cancelWithPermitForAll(nft: string = lensHub.address) {
|
||||
const nftContract = LensHub__factory.connect(nft, testWallet);
|
||||
const name = await nftContract.name();
|
||||
const nonce = (await nftContract.sigNonces(testWallet.address)).toNumber();
|
||||
const { v, r, s } = await getPermitForAllParts(
|
||||
nft,
|
||||
name,
|
||||
testWallet.address,
|
||||
testWallet.address,
|
||||
false,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
await nftContract.permitForAll(testWallet.address, testWallet.address, false, {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPermitParts(
|
||||
nft: string,
|
||||
name: string,
|
||||
spender: string,
|
||||
tokenId: BigNumberish,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
): Promise<{ v: number; r: string; s: string }> {
|
||||
const msgParams = buildPermitParams(nft, name, spender, tokenId, nonce, deadline);
|
||||
return await getSig(msgParams);
|
||||
}
|
||||
|
||||
export async function getPermitForAllParts(
|
||||
nft: string,
|
||||
name: string,
|
||||
owner: string,
|
||||
operator: string,
|
||||
approved: boolean,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
): Promise<{ v: number; r: string; s: string }> {
|
||||
const msgParams = buildPermitForAllParams(nft, name, owner, operator, approved, nonce, deadline);
|
||||
return await getSig(msgParams);
|
||||
}
|
||||
|
||||
export async function getBurnWithSigparts(
|
||||
nft: string,
|
||||
name: string,
|
||||
tokenId: BigNumberish,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
): Promise<{ v: number; r: string; s: string }> {
|
||||
const msgParams = buildBurnWithSigParams(nft, name, tokenId, nonce, deadline);
|
||||
return await getSig(msgParams);
|
||||
}
|
||||
|
||||
export async function getDelegateBySigParts(
|
||||
nft: string,
|
||||
name: string,
|
||||
delegator: string,
|
||||
delegatee: string,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
): Promise<{ v: number; r: string; s: string }> {
|
||||
const msgParams = buildDelegateBySigParams(nft, name, delegator, delegatee, nonce, deadline);
|
||||
return await getSig(msgParams);
|
||||
}
|
||||
|
||||
const buildDelegateBySigParams = (
|
||||
nft: string,
|
||||
name: string,
|
||||
delegator: string,
|
||||
delegatee: string,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
) => ({
|
||||
types: {
|
||||
DelegateBySig: [
|
||||
{ name: 'delegator', type: 'address' },
|
||||
{ name: 'delegatee', type: 'address' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'deadline', type: 'uint256' },
|
||||
],
|
||||
},
|
||||
domain: {
|
||||
name: name,
|
||||
version: '1',
|
||||
chainId: getChainId(),
|
||||
verifyingContract: nft,
|
||||
},
|
||||
value: {
|
||||
delegator: delegator,
|
||||
delegatee: delegatee,
|
||||
nonce: nonce,
|
||||
deadline: deadline,
|
||||
},
|
||||
});
|
||||
|
||||
export async function getSetFollowModuleWithSigParts(
|
||||
profileId: BigNumberish,
|
||||
followModule: string,
|
||||
followModuleData: Bytes | string,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
): Promise<{ v: number; r: string; s: string }> {
|
||||
const msgParams = buildSetFollowModuleWithSigParams(
|
||||
profileId,
|
||||
followModule,
|
||||
followModuleData,
|
||||
nonce,
|
||||
deadline
|
||||
);
|
||||
return await getSig(msgParams);
|
||||
}
|
||||
|
||||
export async function getSetDispatcherWithSigParts(
|
||||
profileId: BigNumberish,
|
||||
dispatcher: string,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
): Promise<{ v: number; r: string; s: string }> {
|
||||
const msgParams = buildSetDispatcherWithSigParams(profileId, dispatcher, nonce, deadline);
|
||||
return await getSig(msgParams);
|
||||
}
|
||||
|
||||
export async function getSetProfileImageURIWithSigParts(
|
||||
profileId: BigNumberish,
|
||||
imageURI: string,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
): Promise<{ v: number; r: string; s: string }> {
|
||||
const msgParams = buildSetProfileImageURIWithSigParams(profileId, imageURI, nonce, deadline);
|
||||
return await getSig(msgParams);
|
||||
}
|
||||
|
||||
export async function getSetFollowNFTURIWithSigParts(
|
||||
profileId: BigNumberish,
|
||||
followNFTURI: string,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
): Promise<{ v: number; r: string; s: string }> {
|
||||
const msgParams = buildSetFollowNFTURIWithSigParams(profileId, followNFTURI, nonce, deadline);
|
||||
return await getSig(msgParams);
|
||||
}
|
||||
|
||||
export async function getPostWithSigParts(
|
||||
profileId: BigNumberish,
|
||||
contentURI: string,
|
||||
collectModule: string,
|
||||
collectModuleData: Bytes | string,
|
||||
referenceModule: string,
|
||||
referenceModuleData: Bytes | string,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
): Promise<{ v: number; r: string; s: string }> {
|
||||
const msgParams = buildPostWithSigParams(
|
||||
profileId,
|
||||
contentURI,
|
||||
collectModule,
|
||||
collectModuleData,
|
||||
referenceModule,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
deadline
|
||||
);
|
||||
return await getSig(msgParams);
|
||||
}
|
||||
|
||||
export async function getCommentWithSigParts(
|
||||
profileId: BigNumberish,
|
||||
contentURI: string,
|
||||
profileIdPointed: BigNumberish,
|
||||
pubIdPointed: string,
|
||||
collectModule: string,
|
||||
collectModuleData: Bytes | string,
|
||||
referenceModule: string,
|
||||
referenceModuleData: Bytes | string,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
): Promise<{ v: number; r: string; s: string }> {
|
||||
const msgParams = buildCommentWithSigParams(
|
||||
profileId,
|
||||
contentURI,
|
||||
profileIdPointed,
|
||||
pubIdPointed,
|
||||
collectModule,
|
||||
collectModuleData,
|
||||
referenceModule,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
deadline
|
||||
);
|
||||
return await getSig(msgParams);
|
||||
}
|
||||
|
||||
export async function getMirrorWithSigParts(
|
||||
profileId: BigNumberish,
|
||||
profileIdPointed: BigNumberish,
|
||||
pubIdPointed: string,
|
||||
referenceModule: string,
|
||||
referenceModuleData: Bytes | string,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
): Promise<{ v: number; r: string; s: string }> {
|
||||
const msgParams = buildMirrorWithSigParams(
|
||||
profileId,
|
||||
profileIdPointed,
|
||||
pubIdPointed,
|
||||
referenceModule,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
deadline
|
||||
);
|
||||
return await getSig(msgParams);
|
||||
}
|
||||
|
||||
export async function getFollowWithSigParts(
|
||||
profileIds: string[] | number[],
|
||||
datas: Bytes[] | string[],
|
||||
nonce: number,
|
||||
deadline: string
|
||||
): Promise<{ v: number; r: string; s: string }> {
|
||||
const msgParams = buildFollowWithSigParams(profileIds, datas, nonce, deadline);
|
||||
return await getSig(msgParams);
|
||||
}
|
||||
|
||||
export async function getCollectWithSigParts(
|
||||
profileId: BigNumberish,
|
||||
pubId: string,
|
||||
data: Bytes | string,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
): Promise<{ v: number; r: string; s: string }> {
|
||||
const msgParams = buildCollectWithSigParams(profileId, pubId, data, nonce, deadline);
|
||||
return await getSig(msgParams);
|
||||
}
|
||||
|
||||
// Modified from AaveTokenV2 repo
|
||||
const buildPermitParams = (
|
||||
nft: string,
|
||||
name: string,
|
||||
spender: string,
|
||||
tokenId: BigNumberish,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
) => ({
|
||||
types: {
|
||||
Permit: [
|
||||
{ name: 'spender', type: 'address' },
|
||||
{ name: 'tokenId', type: 'uint256' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'deadline', type: 'uint256' },
|
||||
],
|
||||
},
|
||||
domain: {
|
||||
name: name,
|
||||
version: '1',
|
||||
chainId: getChainId(),
|
||||
verifyingContract: nft,
|
||||
},
|
||||
value: {
|
||||
spender: spender,
|
||||
tokenId: tokenId,
|
||||
nonce: nonce,
|
||||
deadline: deadline,
|
||||
},
|
||||
});
|
||||
|
||||
const buildPermitForAllParams = (
|
||||
nft: string,
|
||||
name: string,
|
||||
owner: string,
|
||||
operator: string,
|
||||
approved: boolean,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
) => ({
|
||||
types: {
|
||||
PermitForAll: [
|
||||
{ name: 'owner', type: 'address' },
|
||||
{ name: 'operator', type: 'address' },
|
||||
{ name: 'approved', type: 'bool' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'deadline', type: 'uint256' },
|
||||
],
|
||||
},
|
||||
domain: {
|
||||
name: name,
|
||||
version: '1',
|
||||
chainId: getChainId(),
|
||||
verifyingContract: nft,
|
||||
},
|
||||
value: {
|
||||
owner: owner,
|
||||
operator: operator,
|
||||
approved: approved,
|
||||
nonce: nonce,
|
||||
deadline: deadline,
|
||||
},
|
||||
});
|
||||
|
||||
const buildBurnWithSigParams = (
|
||||
nft: string,
|
||||
name: string,
|
||||
tokenId: BigNumberish,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
) => ({
|
||||
types: {
|
||||
BurnWithSig: [
|
||||
{ name: 'tokenId', type: 'uint256' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'deadline', type: 'uint256' },
|
||||
],
|
||||
},
|
||||
domain: {
|
||||
name: name,
|
||||
version: '1',
|
||||
chainId: getChainId(),
|
||||
verifyingContract: nft,
|
||||
},
|
||||
value: {
|
||||
tokenId: tokenId,
|
||||
nonce: nonce,
|
||||
deadline: deadline,
|
||||
},
|
||||
});
|
||||
|
||||
const buildSetFollowModuleWithSigParams = (
|
||||
profileId: BigNumberish,
|
||||
followModule: string,
|
||||
followModuleData: Bytes | string,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
) => ({
|
||||
types: {
|
||||
SetFollowModuleWithSig: [
|
||||
{ name: 'profileId', type: 'uint256' },
|
||||
{ name: 'followModule', type: 'address' },
|
||||
{ name: 'followModuleData', type: 'bytes' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'deadline', type: 'uint256' },
|
||||
],
|
||||
},
|
||||
domain: domain(),
|
||||
value: {
|
||||
profileId: profileId,
|
||||
followModule: followModule,
|
||||
followModuleData: followModuleData,
|
||||
nonce: nonce,
|
||||
deadline: deadline,
|
||||
},
|
||||
});
|
||||
|
||||
const buildSetDispatcherWithSigParams = (
|
||||
profileId: BigNumberish,
|
||||
dispatcher: string,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
) => ({
|
||||
types: {
|
||||
SetDispatcherWithSig: [
|
||||
{ name: 'profileId', type: 'uint256' },
|
||||
{ name: 'dispatcher', type: 'address' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'deadline', type: 'uint256' },
|
||||
],
|
||||
},
|
||||
domain: domain(),
|
||||
value: {
|
||||
profileId: profileId,
|
||||
dispatcher: dispatcher,
|
||||
nonce: nonce,
|
||||
deadline: deadline,
|
||||
},
|
||||
});
|
||||
|
||||
const buildSetProfileImageURIWithSigParams = (
|
||||
profileId: BigNumberish,
|
||||
imageURI: string,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
) => ({
|
||||
types: {
|
||||
SetProfileImageURIWithSig: [
|
||||
{ name: 'profileId', type: 'uint256' },
|
||||
{ name: 'imageURI', type: 'string' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'deadline', type: 'uint256' },
|
||||
],
|
||||
},
|
||||
domain: domain(),
|
||||
value: {
|
||||
profileId: profileId,
|
||||
imageURI: imageURI,
|
||||
nonce: nonce,
|
||||
deadline: deadline,
|
||||
},
|
||||
});
|
||||
|
||||
const buildSetFollowNFTURIWithSigParams = (
|
||||
profileId: BigNumberish,
|
||||
followNFTURI: string,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
) => ({
|
||||
types: {
|
||||
SetFollowNFTURIWithSig: [
|
||||
{ name: 'profileId', type: 'uint256' },
|
||||
{ name: 'followNFTURI', type: 'string' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'deadline', type: 'uint256' },
|
||||
],
|
||||
},
|
||||
domain: domain(),
|
||||
value: {
|
||||
profileId: profileId,
|
||||
followNFTURI: followNFTURI,
|
||||
nonce: nonce,
|
||||
deadline: deadline,
|
||||
},
|
||||
});
|
||||
|
||||
const buildPostWithSigParams = (
|
||||
profileId: BigNumberish,
|
||||
contentURI: string,
|
||||
collectModule: string,
|
||||
collectModuleData: Bytes | string,
|
||||
referenceModule: string,
|
||||
referenceModuleData: Bytes | string,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
) => ({
|
||||
types: {
|
||||
PostWithSig: [
|
||||
{ name: 'profileId', type: 'uint256' },
|
||||
{ name: 'contentURI', type: 'string' },
|
||||
{ name: 'collectModule', type: 'address' },
|
||||
{ name: 'collectModuleData', type: 'bytes' },
|
||||
{ name: 'referenceModule', type: 'address' },
|
||||
{ name: 'referenceModuleData', type: 'bytes' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'deadline', type: 'uint256' },
|
||||
],
|
||||
},
|
||||
domain: domain(),
|
||||
value: {
|
||||
profileId: profileId,
|
||||
contentURI: contentURI,
|
||||
collectModule: collectModule,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: referenceModule,
|
||||
referenceModuleData: referenceModuleData,
|
||||
nonce: nonce,
|
||||
deadline: deadline,
|
||||
},
|
||||
});
|
||||
|
||||
const buildCommentWithSigParams = (
|
||||
profileId: BigNumberish,
|
||||
contentURI: string,
|
||||
profileIdPointed: BigNumberish,
|
||||
pubIdPointed: string,
|
||||
collectModule: string,
|
||||
collectModuleData: Bytes | string,
|
||||
referenceModule: string,
|
||||
referenceModuleData: Bytes | string,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
) => ({
|
||||
types: {
|
||||
CommentWithSig: [
|
||||
{ name: 'profileId', type: 'uint256' },
|
||||
{ name: 'contentURI', type: 'string' },
|
||||
{ name: 'profileIdPointed', type: 'uint256' },
|
||||
{ name: 'pubIdPointed', type: 'uint256' },
|
||||
{ name: 'collectModule', type: 'address' },
|
||||
{ name: 'collectModuleData', type: 'bytes' },
|
||||
{ name: 'referenceModule', type: 'address' },
|
||||
{ name: 'referenceModuleData', type: 'bytes' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'deadline', type: 'uint256' },
|
||||
],
|
||||
},
|
||||
domain: domain(),
|
||||
value: {
|
||||
profileId: profileId,
|
||||
contentURI: contentURI,
|
||||
profileIdPointed: profileIdPointed,
|
||||
pubIdPointed: pubIdPointed,
|
||||
collectModule: collectModule,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: referenceModule,
|
||||
referenceModuleData: referenceModuleData,
|
||||
nonce: nonce,
|
||||
deadline: deadline,
|
||||
},
|
||||
});
|
||||
|
||||
const buildMirrorWithSigParams = (
|
||||
profileId: BigNumberish,
|
||||
profileIdPointed: BigNumberish,
|
||||
pubIdPointed: string,
|
||||
referenceModule: string,
|
||||
referenceModuleData: Bytes | string,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
) => ({
|
||||
types: {
|
||||
MirrorWithSig: [
|
||||
{ name: 'profileId', type: 'uint256' },
|
||||
{ name: 'profileIdPointed', type: 'uint256' },
|
||||
{ name: 'pubIdPointed', type: 'uint256' },
|
||||
{ name: 'referenceModule', type: 'address' },
|
||||
{ name: 'referenceModuleData', type: 'bytes' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'deadline', type: 'uint256' },
|
||||
],
|
||||
},
|
||||
domain: domain(),
|
||||
value: {
|
||||
profileId: profileId,
|
||||
profileIdPointed: profileIdPointed,
|
||||
pubIdPointed: pubIdPointed,
|
||||
referenceModule: referenceModule,
|
||||
referenceModuleData: referenceModuleData,
|
||||
nonce: nonce,
|
||||
deadline: deadline,
|
||||
},
|
||||
});
|
||||
|
||||
const buildFollowWithSigParams = (
|
||||
profileIds: string[] | number[],
|
||||
datas: Bytes[] | string[],
|
||||
nonce: number,
|
||||
deadline: string
|
||||
) => ({
|
||||
types: {
|
||||
FollowWithSig: [
|
||||
{ name: 'profileIds', type: 'uint256[]' },
|
||||
{ name: 'datas', type: 'bytes[]' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'deadline', type: 'uint256' },
|
||||
],
|
||||
},
|
||||
domain: domain(),
|
||||
value: {
|
||||
profileIds: profileIds,
|
||||
datas: datas,
|
||||
nonce: nonce,
|
||||
deadline: deadline,
|
||||
},
|
||||
});
|
||||
|
||||
const buildCollectWithSigParams = (
|
||||
profileId: BigNumberish,
|
||||
pubId: string,
|
||||
data: Bytes | string,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
) => ({
|
||||
types: {
|
||||
CollectWithSig: [
|
||||
{ name: 'profileId', type: 'uint256' },
|
||||
{ name: 'pubId', type: 'uint256' },
|
||||
{ name: 'data', type: 'bytes' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'deadline', type: 'uint256' },
|
||||
],
|
||||
},
|
||||
domain: domain(),
|
||||
value: {
|
||||
profileId: profileId,
|
||||
pubId: pubId,
|
||||
data: data,
|
||||
nonce: nonce,
|
||||
deadline: deadline,
|
||||
},
|
||||
});
|
||||
|
||||
async function getSig(msgParams: {
|
||||
domain: any;
|
||||
types: any;
|
||||
value: any;
|
||||
}): Promise<{ v: number; r: string; s: string }> {
|
||||
const sig = await testWallet._signTypedData(msgParams.domain, msgParams.types, msgParams.value);
|
||||
return utils.splitSignature(sig);
|
||||
}
|
||||
|
||||
function domain(): { name: string; version: string; chainId: number; verifyingContract: string } {
|
||||
return {
|
||||
name: LENS_HUB_NFT_NAME,
|
||||
version: '1',
|
||||
chainId: getChainId(),
|
||||
verifyingContract: lensHub.address,
|
||||
};
|
||||
}
|
||||
457
test/hub/interactions/collecting.spec.ts
Normal file
457
test/hub/interactions/collecting.spec.ts
Normal file
@@ -0,0 +1,457 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { CollectNFT__factory, FollowNFT__factory } from '../../../typechain-types';
|
||||
import { MAX_UINT256, ZERO_ADDRESS } from '../../helpers/constants';
|
||||
import { ERRORS } from '../../helpers/errors';
|
||||
import {
|
||||
cancelWithPermitForAll,
|
||||
getAbbreviation,
|
||||
getCollectWithSigParts,
|
||||
getTimestamp,
|
||||
} from '../../helpers/utils';
|
||||
import {
|
||||
lensHub,
|
||||
emptyCollectModule,
|
||||
FIRST_PROFILE_ID,
|
||||
governance,
|
||||
makeSuiteCleanRoom,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
MOCK_URI,
|
||||
testWallet,
|
||||
userAddress,
|
||||
userTwo,
|
||||
userTwoAddress,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
} from '../../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Collecting', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Generic', function () {
|
||||
context('Negatives', function () {
|
||||
it('UserTwo should fail to collect without being a follower', async function () {
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, [])).to.be.revertedWith(
|
||||
ERRORS.FOLLOW_INVALID
|
||||
);
|
||||
});
|
||||
|
||||
it('user two should follow, then transfer the followNFT and fail to collect', async function () {
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const followNftAddr = await lensHub.getFollowNFT(FIRST_PROFILE_ID);
|
||||
await expect(
|
||||
FollowNFT__factory.connect(followNftAddr, userTwo).transferFrom(
|
||||
userTwoAddress,
|
||||
userAddress,
|
||||
1
|
||||
)
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, [])).to.be.revertedWith(
|
||||
ERRORS.FOLLOW_INVALID
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('UserTwo should follow, then collect, receive a collect NFT with the expected properties', async function () {
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, [])).to.not.be.reverted;
|
||||
const timestamp = await getTimestamp();
|
||||
|
||||
const collectNFTAddr = await lensHub.getCollectNFT(FIRST_PROFILE_ID, 1);
|
||||
expect(collectNFTAddr).to.not.eq(ZERO_ADDRESS);
|
||||
const collectNFT = CollectNFT__factory.connect(collectNFTAddr, userTwo);
|
||||
const id = await collectNFT.tokenOfOwnerByIndex(userTwoAddress, 0);
|
||||
const name = await collectNFT.name();
|
||||
const symbol = await collectNFT.symbol();
|
||||
const pointer = await collectNFT.getSourcePublicationPointer();
|
||||
const owner = await collectNFT.ownerOf(id);
|
||||
const mintTimestamp = await collectNFT.mintTimestampOf(id);
|
||||
const tokenData = await collectNFT.tokenDataOf(id);
|
||||
|
||||
const expectedName = MOCK_PROFILE_HANDLE + '-Collect-' + '1';
|
||||
const expectedSymbol = getAbbreviation(MOCK_PROFILE_HANDLE) + '-Cl-' + '1';
|
||||
|
||||
expect(id).to.eq(1);
|
||||
expect(name).to.eq(expectedName);
|
||||
expect(symbol).to.eq(expectedSymbol);
|
||||
expect(pointer[0]).to.eq(FIRST_PROFILE_ID);
|
||||
expect(pointer[1]).to.eq(1);
|
||||
expect(owner).to.eq(userTwoAddress);
|
||||
expect(tokenData.owner).to.eq(userTwoAddress);
|
||||
expect(tokenData.mintTimestamp).to.eq(timestamp);
|
||||
expect(mintTimestamp).to.eq(timestamp);
|
||||
});
|
||||
|
||||
it('UserTwo should follow, then mirror, then collect on their mirror, receive a collect NFT with expected properties', async function () {
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'mockhandle',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, [])).to.not.be.reverted;
|
||||
|
||||
const collectNFTAddr = await lensHub.getCollectNFT(FIRST_PROFILE_ID, 1);
|
||||
expect(collectNFTAddr).to.not.eq(ZERO_ADDRESS);
|
||||
const collectNFT = CollectNFT__factory.connect(collectNFTAddr, userTwo);
|
||||
const id = await collectNFT.tokenOfOwnerByIndex(userTwoAddress, 0);
|
||||
const name = await collectNFT.name();
|
||||
const symbol = await collectNFT.symbol();
|
||||
const pointer = await collectNFT.getSourcePublicationPointer();
|
||||
|
||||
const expectedName = MOCK_PROFILE_HANDLE + '-Collect-' + '1';
|
||||
const expectedSymbol = getAbbreviation(MOCK_PROFILE_HANDLE) + '-Cl-' + '1';
|
||||
expect(id).to.eq(1);
|
||||
expect(name).to.eq(expectedName);
|
||||
expect(symbol).to.eq(expectedSymbol);
|
||||
expect(pointer[0]).to.eq(FIRST_PROFILE_ID);
|
||||
expect(pointer[1]).to.eq(1);
|
||||
});
|
||||
|
||||
it('UserTwo should follow, then mirror, mirror their mirror then collect on their latest mirror, receive a collect NFT with expected properties', async function () {
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'mockhandle',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: secondProfileId,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 2, [])).to.not.be.reverted;
|
||||
|
||||
const collectNFTAddr = await lensHub.getCollectNFT(FIRST_PROFILE_ID, 1);
|
||||
expect(collectNFTAddr).to.not.eq(ZERO_ADDRESS);
|
||||
const collectNFT = CollectNFT__factory.connect(collectNFTAddr, userTwo);
|
||||
const id = await collectNFT.tokenOfOwnerByIndex(userTwoAddress, 0);
|
||||
const name = await collectNFT.name();
|
||||
const symbol = await collectNFT.symbol();
|
||||
const pointer = await collectNFT.getSourcePublicationPointer();
|
||||
|
||||
const expectedName = MOCK_PROFILE_HANDLE + '-Collect-' + '1';
|
||||
const expectedSymbol = getAbbreviation(MOCK_PROFILE_HANDLE) + '-Cl-' + '1';
|
||||
expect(id).to.eq(1);
|
||||
expect(name).to.eq(expectedName);
|
||||
expect(symbol).to.eq(expectedSymbol);
|
||||
expect(pointer[0]).to.eq(FIRST_PROFILE_ID);
|
||||
expect(pointer[1]).to.eq(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Meta-tx', function () {
|
||||
context('Negatives', function () {
|
||||
it('TestWallet should fail to collect with sig with signature deadline mismatch', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getCollectWithSigParts(FIRST_PROFILE_ID, '1', [], nonce, '0');
|
||||
|
||||
await expect(
|
||||
lensHub.collectWithSig({
|
||||
collector: testWallet.address,
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
pubId: '1',
|
||||
data: [],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to collect with sig with invalid deadline', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getCollectWithSigParts(FIRST_PROFILE_ID, '1', [], nonce, '0');
|
||||
|
||||
await expect(
|
||||
lensHub.collectWithSig({
|
||||
collector: testWallet.address,
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
pubId: '1',
|
||||
data: [],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: '0',
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_EXPIRED);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to collect with sig with invalid nonce', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getCollectWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
'1',
|
||||
[],
|
||||
nonce + 1,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.collectWithSig({
|
||||
collector: testWallet.address,
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
pubId: '1',
|
||||
data: [],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to collect with sig without being a follower', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getCollectWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
'1',
|
||||
[],
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.collectWithSig({
|
||||
collector: testWallet.address,
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
pubId: '1',
|
||||
data: [],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should sign attempt to collect with sig, cancel via empty permitForAll, fail to collect with sig', async function () {
|
||||
await expect(
|
||||
lensHub.connect(testWallet).follow([FIRST_PROFILE_ID], [[]])
|
||||
).to.not.be.reverted;
|
||||
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getCollectWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
'1',
|
||||
[],
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await cancelWithPermitForAll();
|
||||
|
||||
await expect(
|
||||
lensHub.collectWithSig({
|
||||
collector: testWallet.address,
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
pubId: '1',
|
||||
data: [],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('TestWallet should follow, then collect with sig, receive a collect NFT with expected properties', async function () {
|
||||
await expect(
|
||||
lensHub.connect(testWallet).follow([FIRST_PROFILE_ID], [[]])
|
||||
).to.not.be.reverted;
|
||||
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getCollectWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
'1',
|
||||
[],
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.collectWithSig({
|
||||
collector: testWallet.address,
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
pubId: '1',
|
||||
data: [],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const collectNFTAddr = await lensHub.getCollectNFT(FIRST_PROFILE_ID, 1);
|
||||
expect(collectNFTAddr).to.not.eq(ZERO_ADDRESS);
|
||||
const collectNFT = CollectNFT__factory.connect(collectNFTAddr, userTwo);
|
||||
const id = await collectNFT.tokenOfOwnerByIndex(testWallet.address, 0);
|
||||
const name = await collectNFT.name();
|
||||
const symbol = await collectNFT.symbol();
|
||||
const pointer = await collectNFT.getSourcePublicationPointer();
|
||||
|
||||
const expectedName = MOCK_PROFILE_HANDLE + '-Collect-' + '1';
|
||||
const expectedSymbol = getAbbreviation(MOCK_PROFILE_HANDLE) + '-Cl-' + '1';
|
||||
expect(id).to.eq(1);
|
||||
expect(name).to.eq(expectedName);
|
||||
expect(symbol).to.eq(expectedSymbol);
|
||||
expect(pointer[0]).to.eq(FIRST_PROFILE_ID);
|
||||
expect(pointer[1]).to.eq(1);
|
||||
});
|
||||
|
||||
it('TestWallet should follow, mirror, then collect with sig on their mirror', async function () {
|
||||
await expect(
|
||||
lensHub.connect(testWallet).follow([FIRST_PROFILE_ID], [[]])
|
||||
).to.not.be.reverted;
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(testWallet).createProfile({
|
||||
to: testWallet.address,
|
||||
handle: 'mockhandle',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(testWallet).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getCollectWithSigParts(
|
||||
secondProfileId.toString(),
|
||||
'1',
|
||||
[],
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.collectWithSig({
|
||||
collector: testWallet.address,
|
||||
profileId: secondProfileId,
|
||||
pubId: '1',
|
||||
data: [],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const collectNFTAddr = await lensHub.getCollectNFT(FIRST_PROFILE_ID, 1);
|
||||
expect(collectNFTAddr).to.not.eq(ZERO_ADDRESS);
|
||||
const collectNFT = CollectNFT__factory.connect(collectNFTAddr, userTwo);
|
||||
const id = await collectNFT.tokenOfOwnerByIndex(testWallet.address, 0);
|
||||
const name = await collectNFT.name();
|
||||
const symbol = await collectNFT.symbol();
|
||||
const pointer = await collectNFT.getSourcePublicationPointer();
|
||||
|
||||
const expectedName = MOCK_PROFILE_HANDLE + '-Collect-' + '1';
|
||||
const expectedSymbol = getAbbreviation(MOCK_PROFILE_HANDLE) + '-Cl-' + '1';
|
||||
expect(id).to.eq(1);
|
||||
expect(name).to.eq(expectedName);
|
||||
expect(symbol).to.eq(expectedSymbol);
|
||||
expect(pointer[0]).to.eq(FIRST_PROFILE_ID);
|
||||
expect(pointer[1]).to.eq(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
288
test/hub/interactions/following.spec.ts
Normal file
288
test/hub/interactions/following.spec.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { FollowNFT__factory } from '../../../typechain-types';
|
||||
import { MAX_UINT256, ZERO_ADDRESS } from '../../helpers/constants';
|
||||
import { ERRORS } from '../../helpers/errors';
|
||||
import {
|
||||
cancelWithPermitForAll,
|
||||
getAbbreviation,
|
||||
getFollowWithSigParts,
|
||||
getTimestamp,
|
||||
} from '../../helpers/utils';
|
||||
import {
|
||||
lensHub,
|
||||
FIRST_PROFILE_ID,
|
||||
makeSuiteCleanRoom,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
testWallet,
|
||||
user,
|
||||
userTwo,
|
||||
userTwoAddress,
|
||||
MOCK_PROFILE_URI,
|
||||
userAddress,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
} from '../../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Following', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
context('Generic', function () {
|
||||
context('Negatives', function () {
|
||||
it('UserTwo should fail to follow a nonexistent profile', async function () {
|
||||
await expect(
|
||||
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID + 1], [[]])
|
||||
).to.be.revertedWith(ERRORS.TOKEN_DOES_NOT_EXIST);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to follow with array mismatch', async function () {
|
||||
await expect(
|
||||
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID, FIRST_PROFILE_ID], [[]])
|
||||
).to.be.revertedWith(ERRORS.ARRAY_MISMATCH);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to follow a profile that has been burned', async function () {
|
||||
await expect(lensHub.burn(FIRST_PROFILE_ID)).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.be.revertedWith(
|
||||
ERRORS.TOKEN_DOES_NOT_EXIST
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('UserTwo should follow profile 1, receive a followNFT with ID 1, followNFT properties should be correct', async function () {
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const timestamp = await getTimestamp();
|
||||
|
||||
const followNFTAddress = await lensHub.getFollowNFT(FIRST_PROFILE_ID);
|
||||
const followNFT = FollowNFT__factory.connect(followNFTAddress, user);
|
||||
expect(followNFT.address).to.not.eq(ZERO_ADDRESS);
|
||||
const id = await followNFT.tokenOfOwnerByIndex(userTwoAddress, 0);
|
||||
const name = await followNFT.name();
|
||||
const symbol = await followNFT.symbol();
|
||||
const owner = await followNFT.ownerOf(id);
|
||||
const mintTimestamp = await followNFT.mintTimestampOf(id);
|
||||
const followNFTURI = await followNFT.tokenURI(id);
|
||||
const tokenData = await followNFT.tokenDataOf(id);
|
||||
|
||||
expect(id).to.eq(1);
|
||||
expect(name).to.eq(MOCK_PROFILE_HANDLE + '-Follower');
|
||||
expect(symbol).to.eq(getAbbreviation(MOCK_PROFILE_HANDLE) + '-Fl');
|
||||
expect(owner).to.eq(userTwoAddress);
|
||||
expect(tokenData.owner).to.eq(userTwoAddress);
|
||||
expect(tokenData.mintTimestamp).to.eq(timestamp);
|
||||
expect(followNFTURI).to.eq(MOCK_FOLLOW_NFT_URI);
|
||||
expect(mintTimestamp).to.eq(timestamp);
|
||||
});
|
||||
|
||||
it('UserTwo should follow profile 1 twice, receiving followNFTs with IDs 1 and 2', async function () {
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const followNFTAddress = await lensHub.getFollowNFT(FIRST_PROFILE_ID);
|
||||
const followNFT = FollowNFT__factory.connect(followNFTAddress, user);
|
||||
const idOne = await followNFT.tokenOfOwnerByIndex(userTwoAddress, 0);
|
||||
const idTwo = await followNFT.tokenOfOwnerByIndex(userTwoAddress, 1);
|
||||
expect(idOne).to.eq(1);
|
||||
expect(idTwo).to.eq(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Meta-tx', function () {
|
||||
context('Negatives', function () {
|
||||
it('TestWallet should fail to follow with sig with signature deadline mismatch', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getFollowWithSigParts([FIRST_PROFILE_ID], [[]], nonce, '0');
|
||||
await expect(
|
||||
lensHub.followWithSig({
|
||||
follower: testWallet.address,
|
||||
profileIds: [FIRST_PROFILE_ID],
|
||||
datas: [],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to follow with sig with invalid deadline', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getFollowWithSigParts([FIRST_PROFILE_ID], [[]], nonce, '0');
|
||||
await expect(
|
||||
lensHub.followWithSig({
|
||||
follower: testWallet.address,
|
||||
profileIds: [FIRST_PROFILE_ID],
|
||||
datas: [[]],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: '0',
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_EXPIRED);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to follow with sig with invalid nonce', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getFollowWithSigParts(
|
||||
[FIRST_PROFILE_ID],
|
||||
[[]],
|
||||
nonce + 1,
|
||||
MAX_UINT256
|
||||
);
|
||||
await expect(
|
||||
lensHub.followWithSig({
|
||||
follower: testWallet.address,
|
||||
profileIds: [FIRST_PROFILE_ID],
|
||||
datas: [[]],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to follow a nonexistent profile with sig', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getFollowWithSigParts(
|
||||
[FIRST_PROFILE_ID + 1],
|
||||
[[]],
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
await expect(
|
||||
lensHub.followWithSig({
|
||||
follower: testWallet.address,
|
||||
profileIds: [FIRST_PROFILE_ID + 1],
|
||||
datas: [[]],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.TOKEN_DOES_NOT_EXIST);
|
||||
});
|
||||
|
||||
it('TestWallet should sign attempt to follow with sig, cancel with empty permitForAll, then fail to follow with sig', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getFollowWithSigParts(
|
||||
[FIRST_PROFILE_ID],
|
||||
[[]],
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await cancelWithPermitForAll();
|
||||
|
||||
await expect(
|
||||
lensHub.followWithSig({
|
||||
follower: testWallet.address,
|
||||
profileIds: [FIRST_PROFILE_ID],
|
||||
datas: [[]],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('TestWallet should follow profile 1 with sig, receive a follow NFT with ID 1, follow NFT name and symbol should be correct', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getFollowWithSigParts(
|
||||
[FIRST_PROFILE_ID],
|
||||
[[]],
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.followWithSig({
|
||||
follower: testWallet.address,
|
||||
profileIds: [FIRST_PROFILE_ID],
|
||||
datas: [[]],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const followNFTAddress = await lensHub.getFollowNFT(FIRST_PROFILE_ID);
|
||||
const followNFT = FollowNFT__factory.connect(followNFTAddress, user);
|
||||
const id = await followNFT.tokenOfOwnerByIndex(testWallet.address, 0);
|
||||
expect(id).to.eq(1);
|
||||
const name = await followNFT.name();
|
||||
const symbol = await followNFT.symbol();
|
||||
expect(name).to.eq(MOCK_PROFILE_HANDLE + '-Follower');
|
||||
expect(symbol).to.eq(getAbbreviation(MOCK_PROFILE_HANDLE) + '-Fl');
|
||||
});
|
||||
|
||||
it('TestWallet should follow profile 1 with sig twice, receive follow NFTs with IDs 1 and 2', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getFollowWithSigParts(
|
||||
[FIRST_PROFILE_ID, FIRST_PROFILE_ID],
|
||||
[[], []],
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.followWithSig({
|
||||
follower: testWallet.address,
|
||||
profileIds: [FIRST_PROFILE_ID, FIRST_PROFILE_ID],
|
||||
datas: [[], []],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const followNFTAddress = await lensHub.getFollowNFT(FIRST_PROFILE_ID);
|
||||
const followNFT = FollowNFT__factory.connect(followNFTAddress, user);
|
||||
const idOne = await followNFT.tokenOfOwnerByIndex(testWallet.address, 0);
|
||||
const idTwo = await followNFT.tokenOfOwnerByIndex(testWallet.address, 1);
|
||||
expect(idOne).to.eq(1);
|
||||
expect(idTwo).to.eq(2);
|
||||
const name = await followNFT.name();
|
||||
const symbol = await followNFT.symbol();
|
||||
expect(name).to.eq(MOCK_PROFILE_HANDLE + '-Follower');
|
||||
expect(symbol).to.eq(getAbbreviation(MOCK_PROFILE_HANDLE) + '-Fl');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
55
test/hub/interactions/governance.spec.ts
Normal file
55
test/hub/interactions/governance.spec.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { ERRORS } from '../../helpers/errors';
|
||||
import { governance, lensHub, makeSuiteCleanRoom, userAddress } from '../../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Governance Functions', function () {
|
||||
context('Negatives', function () {
|
||||
it('User should not be able to call governance functions', async function () {
|
||||
await expect(lensHub.setGovernance(userAddress)).to.be.revertedWith(ERRORS.NOT_GOVERNANCE);
|
||||
await expect(lensHub.whitelistFollowModule(userAddress, true)).to.be.revertedWith(
|
||||
ERRORS.NOT_GOVERNANCE
|
||||
);
|
||||
await expect(lensHub.whitelistReferenceModule(userAddress, true)).to.be.revertedWith(
|
||||
ERRORS.NOT_GOVERNANCE
|
||||
);
|
||||
await expect(lensHub.whitelistCollectModule(userAddress, true)).to.be.revertedWith(
|
||||
ERRORS.NOT_GOVERNANCE
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('Governance should successfully whitelist and unwhitelist modules', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistFollowModule(userAddress, true)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistReferenceModule(userAddress, true)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(userAddress, true)
|
||||
).to.not.be.reverted;
|
||||
expect(await lensHub.isFollowModuleWhitelisted(userAddress)).to.eq(true);
|
||||
expect(await lensHub.isReferenceModuleWhitelisted(userAddress)).to.eq(true);
|
||||
expect(await lensHub.isCollectModuleWhitelisted(userAddress)).to.eq(true);
|
||||
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistFollowModule(userAddress, false)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistReferenceModule(userAddress, false)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(userAddress, false)
|
||||
).to.not.be.reverted;
|
||||
expect(await lensHub.isFollowModuleWhitelisted(userAddress)).to.eq(false);
|
||||
expect(await lensHub.isReferenceModuleWhitelisted(userAddress)).to.eq(false);
|
||||
expect(await lensHub.isCollectModuleWhitelisted(userAddress)).to.eq(false);
|
||||
});
|
||||
|
||||
it('Governance should successfully change the governance address', async function () {
|
||||
await expect(lensHub.connect(governance).setGovernance(userAddress)).to.not.be.reverted;
|
||||
});
|
||||
});
|
||||
});
|
||||
1850
test/hub/interactions/multi-state-hub.spec.ts
Normal file
1850
test/hub/interactions/multi-state-hub.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
571
test/hub/interactions/publishing-comments.spec.ts
Normal file
571
test/hub/interactions/publishing-comments.spec.ts
Normal file
@@ -0,0 +1,571 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { MAX_UINT256, ZERO_ADDRESS } from '../../helpers/constants';
|
||||
import { ERRORS } from '../../helpers/errors';
|
||||
import { cancelWithPermitForAll, getCommentWithSigParts } from '../../helpers/utils';
|
||||
import {
|
||||
abiCoder,
|
||||
emptyCollectModule,
|
||||
FIRST_PROFILE_ID,
|
||||
governance,
|
||||
lensHub,
|
||||
makeSuiteCleanRoom,
|
||||
mockReferenceModule,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
MOCK_URI,
|
||||
OTHER_MOCK_URI,
|
||||
testWallet,
|
||||
timedFeeCollectModule,
|
||||
userAddress,
|
||||
userTwo,
|
||||
} from '../../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Publishing Comments', function () {
|
||||
context('Generic', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(timedFeeCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistReferenceModule(mockReferenceModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
it('UserTwo should fail to publish a comment to a profile owned by User', async function () {
|
||||
await expect(
|
||||
lensHub.connect(userTwo).comment({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
collectModule: ZERO_ADDRESS,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.NOT_PROFILE_OWNER_OR_DISPATCHER);
|
||||
});
|
||||
|
||||
it('User should fail to comment with an unwhitelisted collect module', async function () {
|
||||
await expect(
|
||||
lensHub.comment({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
collectModule: ZERO_ADDRESS,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.COLLECT_MODULE_NOT_WHITELISTED);
|
||||
});
|
||||
|
||||
it('User should fail to comment with an unwhitelisted reference module', async function () {
|
||||
await expect(
|
||||
lensHub.comment({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: userAddress,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.REFERENCE_MODULE_NOT_WHITELISTED);
|
||||
});
|
||||
|
||||
it('User should fail to comment with invalid collect module data format', async function () {
|
||||
await expect(
|
||||
lensHub.comment({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
collectModule: timedFeeCollectModule.address,
|
||||
collectModuleData: [0x2, 0x12, 0x20],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.NO_REASON_ABI_DECODE);
|
||||
});
|
||||
|
||||
it('User should fail to comment with invalid reference module data format', async function () {
|
||||
await expect(
|
||||
lensHub.comment({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: mockReferenceModule.address,
|
||||
referenceModuleData: [0x12, 0x23],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.NO_REASON_ABI_DECODE);
|
||||
});
|
||||
|
||||
it('User should fail to comment on a publication that does not exist', async function () {
|
||||
await expect(
|
||||
lensHub.comment({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 2,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.PUBLICATION_DOES_NOT_EXIST);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('User should create a comment with empty collect module data, reference module, and reference module data, fetched comment data should be accurate', async function () {
|
||||
await expect(
|
||||
lensHub.comment({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const pub = await lensHub.getPub(FIRST_PROFILE_ID, 2);
|
||||
expect(pub.profileIdPointed).to.eq(FIRST_PROFILE_ID);
|
||||
expect(pub.pubIdPointed).to.eq(1);
|
||||
expect(pub.contentURI).to.eq(MOCK_URI);
|
||||
expect(pub.collectModule).to.eq(emptyCollectModule.address);
|
||||
expect(pub.collectNFT).to.eq(ZERO_ADDRESS);
|
||||
expect(pub.referenceModule).to.eq(ZERO_ADDRESS);
|
||||
});
|
||||
|
||||
it('User should create a post using the mock reference module as reference module, then comment on that post', async function () {
|
||||
const data = abiCoder.encode(['uint256'], ['1']);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: mockReferenceModule.address,
|
||||
referenceModuleData: data,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.comment({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 2,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Meta-tx', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.connect(testWallet).createProfile({
|
||||
to: testWallet.address,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(testWallet).post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
it('Testwallet should fail to comment with sig with signature deadline mismatch', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const collectModuleData = [];
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getCommentWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_URI,
|
||||
FIRST_PROFILE_ID,
|
||||
'1',
|
||||
ZERO_ADDRESS,
|
||||
collectModuleData,
|
||||
ZERO_ADDRESS,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.commentWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: '1',
|
||||
collectModule: userAddress,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('Testwallet should fail to comment with sig with invalid deadline', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const collectModuleData = [];
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getCommentWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_URI,
|
||||
FIRST_PROFILE_ID,
|
||||
'1',
|
||||
ZERO_ADDRESS,
|
||||
collectModuleData,
|
||||
ZERO_ADDRESS,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.commentWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: '1',
|
||||
collectModule: ZERO_ADDRESS,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: '0',
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_EXPIRED);
|
||||
});
|
||||
|
||||
it('Testwallet should fail to comment with sig with invalid nonce', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const collectModuleData = [];
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getCommentWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_URI,
|
||||
FIRST_PROFILE_ID,
|
||||
'1',
|
||||
ZERO_ADDRESS,
|
||||
collectModuleData,
|
||||
ZERO_ADDRESS,
|
||||
referenceModuleData,
|
||||
nonce + 1,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.commentWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: '1',
|
||||
collectModule: ZERO_ADDRESS,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('Testwallet should fail to comment with sig with unwhitelisted collect module', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const collectModuleData = [];
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getCommentWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_URI,
|
||||
FIRST_PROFILE_ID,
|
||||
'1',
|
||||
ZERO_ADDRESS,
|
||||
collectModuleData,
|
||||
ZERO_ADDRESS,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.commentWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: '1',
|
||||
collectModule: ZERO_ADDRESS,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.COLLECT_MODULE_NOT_WHITELISTED);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to comment with sig with unwhitelisted reference module', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const collectModuleData = [];
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getCommentWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_URI,
|
||||
FIRST_PROFILE_ID,
|
||||
'1',
|
||||
emptyCollectModule.address,
|
||||
collectModuleData,
|
||||
mockReferenceModule.address,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.commentWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: '1',
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: mockReferenceModule.address,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.REFERENCE_MODULE_NOT_WHITELISTED);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to comment with sig on a publication that does not exist', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const collectModuleData = [];
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getCommentWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
OTHER_MOCK_URI,
|
||||
FIRST_PROFILE_ID,
|
||||
'2',
|
||||
emptyCollectModule.address,
|
||||
collectModuleData,
|
||||
ZERO_ADDRESS,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.commentWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: OTHER_MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: '2',
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.PUBLICATION_DOES_NOT_EXIST);
|
||||
});
|
||||
|
||||
it('TestWallet should sign attempt to comment with sig, cancel via empty permitForAll, then fail to comment with sig', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const collectModuleData = [];
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getCommentWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
OTHER_MOCK_URI,
|
||||
FIRST_PROFILE_ID,
|
||||
'1',
|
||||
emptyCollectModule.address,
|
||||
collectModuleData,
|
||||
ZERO_ADDRESS,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await cancelWithPermitForAll();
|
||||
|
||||
await expect(
|
||||
lensHub.commentWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: OTHER_MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: '1',
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('TestWallet should comment with sig, fetched comment data should be accurate', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const collectModuleData = [];
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getCommentWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
OTHER_MOCK_URI,
|
||||
FIRST_PROFILE_ID,
|
||||
'1',
|
||||
emptyCollectModule.address,
|
||||
collectModuleData,
|
||||
ZERO_ADDRESS,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.commentWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: OTHER_MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: '1',
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const pub = await lensHub.getPub(FIRST_PROFILE_ID, 2);
|
||||
expect(pub.profileIdPointed).to.eq(FIRST_PROFILE_ID);
|
||||
expect(pub.pubIdPointed).to.eq(1);
|
||||
expect(pub.contentURI).to.eq(OTHER_MOCK_URI);
|
||||
expect(pub.collectModule).to.eq(emptyCollectModule.address);
|
||||
expect(pub.collectNFT).to.eq(ZERO_ADDRESS);
|
||||
expect(pub.referenceModule).to.eq(ZERO_ADDRESS);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
493
test/hub/interactions/publishing-mirrors.spec.ts
Normal file
493
test/hub/interactions/publishing-mirrors.spec.ts
Normal file
@@ -0,0 +1,493 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { MAX_UINT256, ZERO_ADDRESS } from '../../helpers/constants';
|
||||
import { ERRORS } from '../../helpers/errors';
|
||||
import { cancelWithPermitForAll, getMirrorWithSigParts } from '../../helpers/utils';
|
||||
import {
|
||||
abiCoder,
|
||||
emptyCollectModule,
|
||||
FIRST_PROFILE_ID,
|
||||
governance,
|
||||
lensHub,
|
||||
makeSuiteCleanRoom,
|
||||
mockReferenceModule,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
MOCK_URI,
|
||||
testWallet,
|
||||
userAddress,
|
||||
userTwo,
|
||||
} from '../../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Publishing mirrors', function () {
|
||||
context('Generic', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistReferenceModule(mockReferenceModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
it('UserTwo should fail to publish a mirror to a profile owned by User', async function () {
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.NOT_PROFILE_OWNER_OR_DISPATCHER);
|
||||
});
|
||||
|
||||
it('User should fail to mirror with an unwhitelisted reference module', async function () {
|
||||
await expect(
|
||||
lensHub.mirror({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: userAddress,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.REFERENCE_MODULE_NOT_WHITELISTED);
|
||||
});
|
||||
|
||||
it('User should fail to mirror with invalid reference module data format', async function () {
|
||||
await expect(
|
||||
lensHub.mirror({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: mockReferenceModule.address,
|
||||
referenceModuleData: [0x12, 0x23],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.NO_REASON_ABI_DECODE);
|
||||
});
|
||||
|
||||
it('User should fail to mirror a publication that does not exist', async function () {
|
||||
await expect(
|
||||
lensHub.mirror({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 2,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.PUBLICATION_DOES_NOT_EXIST);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('User should create a mirror with empty reference module and reference module data, fetched mirror data should be accurate', async function () {
|
||||
await expect(
|
||||
lensHub.mirror({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const pub = await lensHub.getPub(FIRST_PROFILE_ID, 2);
|
||||
expect(pub.profileIdPointed).to.eq(FIRST_PROFILE_ID);
|
||||
expect(pub.pubIdPointed).to.eq(1);
|
||||
expect(pub.contentURI).to.eq('');
|
||||
expect(pub.collectModule).to.eq(ZERO_ADDRESS);
|
||||
expect(pub.collectNFT).to.eq(ZERO_ADDRESS);
|
||||
expect(pub.referenceModule).to.eq(ZERO_ADDRESS);
|
||||
});
|
||||
|
||||
it('User should mirror a mirror with empty reference module and reference module data, fetched mirror data should be accurate and point to the original post', async function () {
|
||||
await expect(
|
||||
lensHub.mirror({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.mirror({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 2,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const pub = await lensHub.getPub(FIRST_PROFILE_ID, 3);
|
||||
expect(pub.profileIdPointed).to.eq(FIRST_PROFILE_ID);
|
||||
expect(pub.pubIdPointed).to.eq(1);
|
||||
expect(pub.contentURI).to.eq('');
|
||||
expect(pub.collectModule).to.eq(ZERO_ADDRESS);
|
||||
expect(pub.collectNFT).to.eq(ZERO_ADDRESS);
|
||||
expect(pub.referenceModule).to.eq(ZERO_ADDRESS);
|
||||
});
|
||||
|
||||
it('User should create a post using the mock reference module as reference module, then mirror that post', async function () {
|
||||
const data = abiCoder.encode(['uint256'], ['1']);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: mockReferenceModule.address,
|
||||
referenceModuleData: data,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.mirror({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 2,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Meta-tx', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.connect(testWallet).createProfile({
|
||||
to: testWallet.address,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(testWallet).post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
it('Testwallet should fail to mirror with sig with signature deadline mismatch', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getMirrorWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
FIRST_PROFILE_ID,
|
||||
'1',
|
||||
ZERO_ADDRESS,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.mirrorWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: '1',
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('Testwallet should fail to mirror with sig with invalid deadline', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getMirrorWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
FIRST_PROFILE_ID,
|
||||
'1',
|
||||
ZERO_ADDRESS,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.mirrorWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: '1',
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: '0',
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_EXPIRED);
|
||||
});
|
||||
|
||||
it('Testwallet should fail to mirror with sig with invalid deadline', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getMirrorWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
FIRST_PROFILE_ID,
|
||||
'1',
|
||||
ZERO_ADDRESS,
|
||||
referenceModuleData,
|
||||
nonce + 1,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.mirrorWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: '1',
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('Testwallet should fail to mirror with sig with unwhitelisted reference module', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getMirrorWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
FIRST_PROFILE_ID,
|
||||
'1',
|
||||
userAddress,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.mirrorWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: '1',
|
||||
referenceModule: userAddress,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.REFERENCE_MODULE_NOT_WHITELISTED);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to mirror a publication with sig that does not exist yet', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getMirrorWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
FIRST_PROFILE_ID,
|
||||
'2',
|
||||
ZERO_ADDRESS,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.mirrorWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: '2',
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.PUBLICATION_DOES_NOT_EXIST);
|
||||
});
|
||||
|
||||
it('TestWallet should sign attempt to mirror with sig, cancel via empty permitForAll, then fail to mirror with sig', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getMirrorWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
FIRST_PROFILE_ID,
|
||||
'1',
|
||||
ZERO_ADDRESS,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await cancelWithPermitForAll();
|
||||
|
||||
await expect(
|
||||
lensHub.mirrorWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: '1',
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('Testwallet should mirror with sig, fetched mirror data should be accurate', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getMirrorWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
FIRST_PROFILE_ID,
|
||||
'1',
|
||||
ZERO_ADDRESS,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.mirrorWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: '1',
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const pub = await lensHub.getPub(FIRST_PROFILE_ID, 2);
|
||||
expect(pub.profileIdPointed).to.eq(FIRST_PROFILE_ID);
|
||||
expect(pub.pubIdPointed).to.eq(1);
|
||||
expect(pub.contentURI).to.eq('');
|
||||
expect(pub.collectModule).to.eq(ZERO_ADDRESS);
|
||||
expect(pub.collectNFT).to.eq(ZERO_ADDRESS);
|
||||
expect(pub.referenceModule).to.eq(ZERO_ADDRESS);
|
||||
});
|
||||
|
||||
it('TestWallet should mirror a mirror with sig, fetched mirror data should be accurate', async function () {
|
||||
await expect(
|
||||
lensHub.connect(testWallet).mirror({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getMirrorWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
FIRST_PROFILE_ID,
|
||||
'2',
|
||||
ZERO_ADDRESS,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.mirrorWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: '2',
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const pub = await lensHub.getPub(FIRST_PROFILE_ID, 3);
|
||||
expect(pub.profileIdPointed).to.eq(FIRST_PROFILE_ID);
|
||||
expect(pub.pubIdPointed).to.eq(1);
|
||||
expect(pub.contentURI).to.eq('');
|
||||
expect(pub.collectModule).to.eq(ZERO_ADDRESS);
|
||||
expect(pub.collectNFT).to.eq(ZERO_ADDRESS);
|
||||
expect(pub.referenceModule).to.eq(ZERO_ADDRESS);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
460
test/hub/interactions/publishing-posts.spec.ts
Normal file
460
test/hub/interactions/publishing-posts.spec.ts
Normal file
@@ -0,0 +1,460 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { MAX_UINT256, ZERO_ADDRESS } from '../../helpers/constants';
|
||||
import { ERRORS } from '../../helpers/errors';
|
||||
import { cancelWithPermitForAll, getPostWithSigParts } from '../../helpers/utils';
|
||||
import {
|
||||
emptyCollectModule,
|
||||
FIRST_PROFILE_ID,
|
||||
governance,
|
||||
lensHub,
|
||||
makeSuiteCleanRoom,
|
||||
mockModuleData,
|
||||
mockReferenceModule,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
MOCK_URI,
|
||||
testWallet,
|
||||
timedFeeCollectModule,
|
||||
userAddress,
|
||||
userTwo,
|
||||
} from '../../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Publishing Posts', function () {
|
||||
context('Generic', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
it('UserTwo should fail to post to a profile owned by User', async function () {
|
||||
await expect(
|
||||
lensHub.connect(userTwo).post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.NOT_PROFILE_OWNER_OR_DISPATCHER);
|
||||
});
|
||||
|
||||
it('User should fail to post with an unwhitelisted collect module', async function () {
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.COLLECT_MODULE_NOT_WHITELISTED);
|
||||
});
|
||||
|
||||
it('User should fail to post with an unwhitelisted reference module', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: userAddress,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.REFERENCE_MODULE_NOT_WHITELISTED);
|
||||
});
|
||||
|
||||
it('User should fail to post with invalid collect module data format', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(timedFeeCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: timedFeeCollectModule.address,
|
||||
collectModuleData: [0x12, 0x34],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.NO_REASON_ABI_DECODE);
|
||||
});
|
||||
|
||||
it('User should fail to post with invalid reference module data format', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistReferenceModule(mockReferenceModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: mockReferenceModule.address,
|
||||
referenceModuleData: [0x12, 0x23],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.NO_REASON_ABI_DECODE);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('User should create a post with empty collect and reference module data, fetched post data should be accurate', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const pub = await lensHub.getPub(FIRST_PROFILE_ID, 1);
|
||||
expect(pub.profileIdPointed).to.eq(0);
|
||||
expect(pub.pubIdPointed).to.eq(0);
|
||||
expect(pub.contentURI).to.eq(MOCK_URI);
|
||||
expect(pub.collectModule).to.eq(emptyCollectModule.address);
|
||||
expect(pub.collectNFT).to.eq(ZERO_ADDRESS);
|
||||
expect(pub.referenceModule).to.eq(ZERO_ADDRESS);
|
||||
});
|
||||
|
||||
it('User should create a post with a whitelisted collect and reference module', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistReferenceModule(mockReferenceModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: mockReferenceModule.address,
|
||||
referenceModuleData: mockModuleData,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Meta-tx', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.connect(testWallet).createProfile({
|
||||
to: testWallet.address,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
it('Testwallet should fail to post with sig with signature deadline mismatch', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const collectModuleData = [];
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getPostWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_URI,
|
||||
ZERO_ADDRESS,
|
||||
collectModuleData,
|
||||
ZERO_ADDRESS,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.postWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: ZERO_ADDRESS,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('Testwallet should fail to post with sig with invalid deadline', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const collectModuleData = [];
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getPostWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_URI,
|
||||
ZERO_ADDRESS,
|
||||
collectModuleData,
|
||||
ZERO_ADDRESS,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.postWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: ZERO_ADDRESS,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: '0',
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_EXPIRED);
|
||||
});
|
||||
|
||||
it('Testwallet should fail to post with sig with invalid nonce', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const collectModuleData = [];
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getPostWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_URI,
|
||||
ZERO_ADDRESS,
|
||||
collectModuleData,
|
||||
ZERO_ADDRESS,
|
||||
referenceModuleData,
|
||||
nonce + 1,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.postWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: userAddress,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('Testwallet should fail to post with sig with an unwhitelisted collect module', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const collectModuleData = [];
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getPostWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_URI,
|
||||
userAddress,
|
||||
collectModuleData,
|
||||
ZERO_ADDRESS,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.postWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: userAddress,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.COLLECT_MODULE_NOT_WHITELISTED);
|
||||
});
|
||||
|
||||
it('Testwallet should fail to post with sig with an unwhitelisted reference module', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const collectModuleData = [];
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getPostWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_URI,
|
||||
emptyCollectModule.address,
|
||||
collectModuleData,
|
||||
userAddress,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.postWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: userAddress,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.REFERENCE_MODULE_NOT_WHITELISTED);
|
||||
});
|
||||
|
||||
it('TestWallet should sign attempt to post with sig, cancel via empty permitForAll, then fail to post with sig', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const collectModuleData = [];
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getPostWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_URI,
|
||||
emptyCollectModule.address,
|
||||
collectModuleData,
|
||||
ZERO_ADDRESS,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await cancelWithPermitForAll();
|
||||
|
||||
await expect(
|
||||
lensHub.postWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('TestWallet should post with sig, fetched post data should be accurate', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const collectModuleData = [];
|
||||
const referenceModuleData = [];
|
||||
|
||||
const { v, r, s } = await getPostWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_URI,
|
||||
emptyCollectModule.address,
|
||||
collectModuleData,
|
||||
ZERO_ADDRESS,
|
||||
referenceModuleData,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.postWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: referenceModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const pub = await lensHub.getPub(FIRST_PROFILE_ID, 1);
|
||||
expect(pub.profileIdPointed).to.eq(0);
|
||||
expect(pub.pubIdPointed).to.eq(0);
|
||||
expect(pub.contentURI).to.eq(MOCK_URI);
|
||||
expect(pub.collectModule).to.eq(emptyCollectModule.address);
|
||||
expect(pub.collectNFT).to.eq(ZERO_ADDRESS);
|
||||
expect(pub.referenceModule).to.eq(ZERO_ADDRESS);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
281
test/hub/profiles/dispatcher.spec.ts
Normal file
281
test/hub/profiles/dispatcher.spec.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { MAX_UINT256, ZERO_ADDRESS } from '../../helpers/constants';
|
||||
import { ERRORS } from '../../helpers/errors';
|
||||
import { cancelWithPermitForAll, getSetDispatcherWithSigParts } from '../../helpers/utils';
|
||||
import {
|
||||
emptyCollectModule,
|
||||
FIRST_PROFILE_ID,
|
||||
governance,
|
||||
lensHub,
|
||||
makeSuiteCleanRoom,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
MOCK_URI,
|
||||
testWallet,
|
||||
userAddress,
|
||||
userTwo,
|
||||
userTwoAddress,
|
||||
} from '../../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Dispatcher Functionality', function () {
|
||||
context('Generic', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
it('UserTwo should fail to set dispatcher on profile owned by user 1', async function () {
|
||||
await expect(
|
||||
lensHub.connect(userTwo).setDispatcher(FIRST_PROFILE_ID, userTwoAddress)
|
||||
).to.be.revertedWith(ERRORS.NOT_PROFILE_OWNER);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to publish on profile owned by user 1 without being a dispatcher', async function () {
|
||||
await expect(
|
||||
lensHub.connect(userTwo).post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.NOT_PROFILE_OWNER_OR_DISPATCHER);
|
||||
});
|
||||
|
||||
it("User should set userTwo as dispatcher, userTwo should fail to set follow module on user's profile", async function () {
|
||||
await expect(lensHub.setDispatcher(FIRST_PROFILE_ID, userTwoAddress)).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).setFollowModule(FIRST_PROFILE_ID, ZERO_ADDRESS, [])
|
||||
).to.be.revertedWith(ERRORS.NOT_PROFILE_OWNER);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('User should set user two as a dispatcher on their profile, user two should post, comment and mirror', async function () {
|
||||
await expect(lensHub.setDispatcher(FIRST_PROFILE_ID, userTwoAddress)).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(userTwo).post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(userTwo).comment({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Meta-tx', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.connect(testWallet).createProfile({
|
||||
to: testWallet.address,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
it('TestWallet should fail to set dispatcher with sig with signature deadline mismatch', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const { v, r, s } = await getSetDispatcherWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
userTwoAddress,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.setDispatcherWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
dispatcher: userTwoAddress,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to set dispatcher with sig with invalid deadline', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const { v, r, s } = await getSetDispatcherWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
userTwoAddress,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.setDispatcherWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
dispatcher: userTwoAddress,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: '0',
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_EXPIRED);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to set dispatcher with sig with invalid nonce', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const { v, r, s } = await getSetDispatcherWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
userTwoAddress,
|
||||
nonce + 1,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.setDispatcherWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
dispatcher: userTwoAddress,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should sign attempt to set dispatcher with sig, cancel via empty permitForAll, fail to set dispatcher with sig', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const { v, r, s } = await getSetDispatcherWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
userTwoAddress,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await cancelWithPermitForAll();
|
||||
|
||||
await expect(
|
||||
lensHub.setDispatcherWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
dispatcher: userTwoAddress,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('TestWallet should set user two as dispatcher for their profile, user two should post, comment and mirror', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const { v, r, s } = await getSetDispatcherWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
userTwoAddress,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.setDispatcherWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
dispatcher: userTwoAddress,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(userTwo).post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(userTwo).comment({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
252
test/hub/profiles/profile-creation.spec.ts
Normal file
252
test/hub/profiles/profile-creation.spec.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { BigNumber } from 'ethers';
|
||||
import { TokenDataStructOutput } from '../../../typechain-types/LensHub';
|
||||
import { MAX_UINT256, ZERO_ADDRESS } from '../../helpers/constants';
|
||||
import { ERRORS } from '../../helpers/errors';
|
||||
import { cancelWithPermitForAll, getTimestamp } from '../../helpers/utils';
|
||||
import {
|
||||
FIRST_PROFILE_ID,
|
||||
governance,
|
||||
lensHub,
|
||||
makeSuiteCleanRoom,
|
||||
mockFollowModule,
|
||||
mockModuleData,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
userAddress,
|
||||
userTwo,
|
||||
userTwoAddress,
|
||||
} from '../../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Profile Creation', function () {
|
||||
context('Generic', function () {
|
||||
context('Negatives', function () {
|
||||
it('User should fail to create a profile with a handle longer than 31 bytes', async function () {
|
||||
const val = '11111111111111111111111111111111';
|
||||
expect(val.length).to.eq(32);
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: val,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INVALID_HANDLE_LENGTH);
|
||||
});
|
||||
|
||||
it('User should fail to create a profile with an empty handle (0 length bytes)', async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: '',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INVALID_HANDLE_LENGTH);
|
||||
});
|
||||
|
||||
it('User should fail to create a profile with a handle with a capital letter', async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: 'Egg',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.HANDLE_CONTAINS_INVALID_CHARACTERS);
|
||||
});
|
||||
|
||||
it('User should fail to create a profile with a handle with an invalid character', async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: 'egg?',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.HANDLE_CONTAINS_INVALID_CHARACTERS);
|
||||
});
|
||||
|
||||
it('User should fail to create a profile with a unwhitelisted follow module', async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: userAddress,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.FOLLOW_MODULE_NOT_WHITELISTED);
|
||||
});
|
||||
|
||||
it('User should fail to create a profile with with invalid follow module data format', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistFollowModule(mockFollowModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: mockFollowModule.address,
|
||||
followModuleData: [0x12, 0x34],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.NO_REASON_ABI_DECODE);
|
||||
});
|
||||
|
||||
it('User should fail to createa a profile when they are not a whitelisted profile creator', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistProfileCreator(userAddress, false)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.PROFILE_CREATOR_NOT_WHITELISTED);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('User should be able to create a profile with a handle, receive an NFT and the handle should resolve to the NFT ID, userTwo should do the same', async function () {
|
||||
let timestamp: any;
|
||||
let owner: string;
|
||||
let totalSupply: BigNumber;
|
||||
let profileId: BigNumber;
|
||||
let mintTimestamp: BigNumber;
|
||||
let tokenData: TokenDataStructOutput;
|
||||
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
timestamp = await getTimestamp();
|
||||
owner = await lensHub.ownerOf(FIRST_PROFILE_ID);
|
||||
totalSupply = await lensHub.totalSupply();
|
||||
profileId = await lensHub.getProfileIdByHandle(MOCK_PROFILE_HANDLE);
|
||||
mintTimestamp = await lensHub.mintTimestampOf(FIRST_PROFILE_ID);
|
||||
tokenData = await lensHub.tokenDataOf(FIRST_PROFILE_ID);
|
||||
expect(owner).to.eq(userAddress);
|
||||
expect(totalSupply).to.eq(FIRST_PROFILE_ID);
|
||||
expect(profileId).to.eq(FIRST_PROFILE_ID);
|
||||
expect(mintTimestamp).to.eq(timestamp);
|
||||
expect(tokenData.owner).to.eq(userAddress);
|
||||
expect(tokenData.mintTimestamp).to.eq(timestamp);
|
||||
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'test',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
timestamp = await getTimestamp();
|
||||
owner = await lensHub.ownerOf(secondProfileId);
|
||||
totalSupply = await lensHub.totalSupply();
|
||||
profileId = await lensHub.getProfileIdByHandle('test');
|
||||
mintTimestamp = await lensHub.mintTimestampOf(secondProfileId);
|
||||
tokenData = await lensHub.tokenDataOf(secondProfileId);
|
||||
expect(owner).to.eq(userTwoAddress);
|
||||
expect(totalSupply).to.eq(secondProfileId);
|
||||
expect(profileId).to.eq(secondProfileId);
|
||||
expect(mintTimestamp).to.eq(timestamp);
|
||||
expect(tokenData.owner).to.eq(userTwoAddress);
|
||||
expect(tokenData.mintTimestamp).to.eq(timestamp);
|
||||
});
|
||||
|
||||
it('User should be able to create a profile with a handle 16 bytes long, then fail to create with the same handle, and create again with a different handle', async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: '123456789012345',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: '123456789012345',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.PROFILE_HANDLE_TAKEN);
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: 'abcdefghijklmno',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('User should be able to create a profile with a whitelisted follow module', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistFollowModule(mockFollowModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: mockFollowModule.address,
|
||||
followModuleData: mockModuleData,
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('User should create a profile for userTwo', async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
expect(await lensHub.ownerOf(FIRST_PROFILE_ID)).to.eq(userTwoAddress);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
340
test/hub/profiles/profile-uri.spec.ts
Normal file
340
test/hub/profiles/profile-uri.spec.ts
Normal file
@@ -0,0 +1,340 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { FollowNFT__factory } from '../../../typechain-types';
|
||||
import { MAX_UINT256, ZERO_ADDRESS } from '../../helpers/constants';
|
||||
import { ERRORS } from '../../helpers/errors';
|
||||
import {
|
||||
cancelWithPermitForAll,
|
||||
getSetFollowNFTURIWithSigParts,
|
||||
getSetProfileImageURIWithSigParts,
|
||||
} from '../../helpers/utils';
|
||||
import {
|
||||
FIRST_PROFILE_ID,
|
||||
lensHub,
|
||||
makeSuiteCleanRoom,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
MOCK_URI,
|
||||
OTHER_MOCK_URI,
|
||||
testWallet,
|
||||
user,
|
||||
userAddress,
|
||||
userTwo,
|
||||
userTwoAddress,
|
||||
} from '../../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Profile URI Functionality', function () {
|
||||
context('Generic', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
it('UserTwo should fail to set the profile URI on profile owned by user 1', async function () {
|
||||
await expect(
|
||||
lensHub.connect(userTwo).setProfileImageURI(FIRST_PROFILE_ID, MOCK_URI)
|
||||
).to.be.revertedWith(ERRORS.NOT_PROFILE_OWNER_OR_DISPATCHER);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to change the follow NFT URI for profile one', async function () {
|
||||
await expect(
|
||||
lensHub.connect(userTwo).setFollowNFTURI(FIRST_PROFILE_ID, OTHER_MOCK_URI)
|
||||
).to.be.revertedWith(ERRORS.NOT_PROFILE_OWNER_OR_DISPATCHER);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('User should set the profile URI', async function () {
|
||||
await expect(lensHub.setProfileImageURI(FIRST_PROFILE_ID, MOCK_URI)).to.not.be.reverted;
|
||||
expect(await lensHub.tokenURI(FIRST_PROFILE_ID)).to.eq(MOCK_URI);
|
||||
});
|
||||
|
||||
it('User should set user two as a dispatcher on their profile, user two should set the profile URI', async function () {
|
||||
await expect(lensHub.setDispatcher(FIRST_PROFILE_ID, userTwoAddress)).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).setProfileImageURI(FIRST_PROFILE_ID, MOCK_URI)
|
||||
).to.not.be.reverted;
|
||||
expect(await lensHub.tokenURI(FIRST_PROFILE_ID)).to.eq(MOCK_URI);
|
||||
});
|
||||
|
||||
it('User should follow profile 1, user should change the follow NFT URI, URI is accurate before and after the change', async function () {
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const followNFTAddress = await lensHub.getFollowNFT(FIRST_PROFILE_ID);
|
||||
const followNFT = FollowNFT__factory.connect(followNFTAddress, user);
|
||||
|
||||
const uriBefore = await followNFT.tokenURI(1);
|
||||
expect(uriBefore).to.eq(MOCK_FOLLOW_NFT_URI);
|
||||
|
||||
await expect(lensHub.setFollowNFTURI(FIRST_PROFILE_ID, OTHER_MOCK_URI)).to.not.be.reverted;
|
||||
|
||||
const uriAfter = await followNFT.tokenURI(1);
|
||||
expect(uriAfter).to.eq(OTHER_MOCK_URI);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Meta-tx', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.connect(testWallet).createProfile({
|
||||
to: testWallet.address,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
it('TestWallet should fail to set profile URI with sig with signature deadline mismatch', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const { v, r, s } = await getSetProfileImageURIWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_URI,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.setProfileImageURIWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
imageURI: MOCK_URI,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to set profile URI with sig with invalid deadline', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const { v, r, s } = await getSetProfileImageURIWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_URI,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.setProfileImageURIWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
imageURI: MOCK_URI,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: '0',
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_EXPIRED);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to set profile URI with sig with invalid nonce', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const { v, r, s } = await getSetProfileImageURIWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_URI,
|
||||
nonce + 1,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.setProfileImageURIWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
imageURI: MOCK_URI,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should sign attempt to set profile URI with sig, cancel with empty permitForAll, then fail to set profile URI with sig', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const { v, r, s } = await getSetProfileImageURIWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_URI,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await cancelWithPermitForAll();
|
||||
|
||||
await expect(
|
||||
lensHub.setProfileImageURIWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
imageURI: MOCK_URI,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to set the follow NFT URI with sig with signature deadline mismatch', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const { v, r, s } = await getSetFollowNFTURIWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_URI,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.setFollowNFTURIWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
followNFTURI: MOCK_URI,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to set the follow NFT URI with sig with invalid deadline', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const { v, r, s } = await getSetFollowNFTURIWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_URI,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.setFollowNFTURIWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
followNFTURI: MOCK_URI,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: '0',
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_EXPIRED);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to set the follow NFT URI with sig with invalid nonce', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const { v, r, s } = await getSetFollowNFTURIWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_URI,
|
||||
nonce + 1,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.setFollowNFTURIWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
followNFTURI: MOCK_URI,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should sign attempt to set follow NFT URI with sig, cancel with empty permitForAll, then fail to set follow NFT URI with sig', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const { v, r, s } = await getSetFollowNFTURIWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_URI,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await cancelWithPermitForAll();
|
||||
|
||||
await expect(
|
||||
lensHub.setFollowNFTURIWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
followNFTURI: MOCK_URI,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('TestWallet should set the profile URI with sig', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const { v, r, s } = await getSetProfileImageURIWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_URI,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.setProfileImageURIWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
imageURI: MOCK_URI,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
expect(await lensHub.tokenURI(FIRST_PROFILE_ID)).to.eq(MOCK_URI);
|
||||
});
|
||||
|
||||
it('TestWallet should set the follow NFT URI with sig', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const { v, r, s } = await getSetFollowNFTURIWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_URI,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.setFollowNFTURIWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
followNFTURI: MOCK_URI,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
281
test/hub/profiles/setting-follow-module.spec.ts
Normal file
281
test/hub/profiles/setting-follow-module.spec.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { MAX_UINT256, ZERO_ADDRESS } from '../../helpers/constants';
|
||||
import { ERRORS } from '../../helpers/errors';
|
||||
import { cancelWithPermitForAll, getSetFollowModuleWithSigParts } from '../../helpers/utils';
|
||||
import {
|
||||
FIRST_PROFILE_ID,
|
||||
governance,
|
||||
lensHub,
|
||||
makeSuiteCleanRoom,
|
||||
mockFollowModule,
|
||||
mockModuleData,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
testWallet,
|
||||
userAddress,
|
||||
userTwo,
|
||||
} from '../../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Setting Follow Module', function () {
|
||||
context('Generic', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
it('UserTwo should fail to set the follow module for the profile owned by User', async function () {
|
||||
await expect(
|
||||
lensHub.connect(userTwo).setFollowModule(FIRST_PROFILE_ID, userAddress, [])
|
||||
).to.be.revertedWith(ERRORS.NOT_PROFILE_OWNER);
|
||||
});
|
||||
|
||||
it('User should fail to set a follow module that is not whitelisted', async function () {
|
||||
await expect(lensHub.setFollowModule(FIRST_PROFILE_ID, userAddress, [])).to.be.revertedWith(
|
||||
ERRORS.FOLLOW_MODULE_NOT_WHITELISTED
|
||||
);
|
||||
});
|
||||
|
||||
it('User should fail to set a follow module with invalid follow module data format', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistFollowModule(mockFollowModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.setFollowModule(FIRST_PROFILE_ID, mockFollowModule.address, [0x12, 0x34])
|
||||
).to.be.revertedWith(ERRORS.NO_REASON_ABI_DECODE);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('User should set a whitelisted follow module, fetching the profile follow module should return the correct address, user then sets it to the zero address and fetching returns the zero address', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistFollowModule(mockFollowModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.setFollowModule(FIRST_PROFILE_ID, mockFollowModule.address, mockModuleData)
|
||||
).to.not.be.reverted;
|
||||
expect(await lensHub.getFollowModule(FIRST_PROFILE_ID)).to.eq(mockFollowModule.address);
|
||||
|
||||
await expect(
|
||||
lensHub.setFollowModule(FIRST_PROFILE_ID, ZERO_ADDRESS, [])
|
||||
).to.not.be.reverted;
|
||||
expect(await lensHub.getFollowModule(FIRST_PROFILE_ID)).to.eq(ZERO_ADDRESS);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Meta-tx', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.connect(testWallet).createProfile({
|
||||
to: testWallet.address,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
it('TestWallet should fail to set a follow module with sig with signature deadline mismatch', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistFollowModule(mockFollowModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const followModuleData = [];
|
||||
|
||||
const { v, r, s } = await getSetFollowModuleWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
mockFollowModule.address,
|
||||
followModuleData,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.setFollowModuleWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
followModule: mockFollowModule.address,
|
||||
followModuleData: followModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to set a follow module with sig with invalid deadline', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistFollowModule(mockFollowModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const followModuleData = [];
|
||||
|
||||
const { v, r, s } = await getSetFollowModuleWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
mockFollowModule.address,
|
||||
followModuleData,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.setFollowModuleWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
followModule: mockFollowModule.address,
|
||||
followModuleData: followModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: '0',
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_EXPIRED);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to set a follow module with sig with invalid nonce', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistFollowModule(mockFollowModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const followModuleData = [];
|
||||
|
||||
const { v, r, s } = await getSetFollowModuleWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
mockFollowModule.address,
|
||||
followModuleData,
|
||||
nonce + 1,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.setFollowModuleWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
followModule: mockFollowModule.address,
|
||||
followModuleData: followModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to set a follow module with sig with an unwhitelisted follow module', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
const followModuleData = [];
|
||||
|
||||
const { v, r, s } = await getSetFollowModuleWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
mockFollowModule.address,
|
||||
followModuleData,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.setFollowModuleWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
followModule: mockFollowModule.address,
|
||||
followModuleData: followModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.FOLLOW_MODULE_NOT_WHITELISTED);
|
||||
});
|
||||
|
||||
it('TestWallet should sign attempt to set follow module with sig, then cancel with empty permitForAll, then fail to set follow module with sig', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistFollowModule(mockFollowModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getSetFollowModuleWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
mockFollowModule.address,
|
||||
mockModuleData,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await cancelWithPermitForAll();
|
||||
|
||||
await expect(
|
||||
lensHub.setFollowModuleWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
followModule: mockFollowModule.address,
|
||||
followModuleData: mockModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('TestWallet should set a whitelisted follow module with sig, fetching the profile follow module should return the correct address', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistFollowModule(mockFollowModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getSetFollowModuleWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
mockFollowModule.address,
|
||||
mockModuleData,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.setFollowModuleWithSig({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
followModule: mockFollowModule.address,
|
||||
followModuleData: mockModuleData,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
expect(await lensHub.getFollowModule(FIRST_PROFILE_ID)).to.eq(mockFollowModule.address);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
201
test/modules/collect/empty-collect-module.spec.ts
Normal file
201
test/modules/collect/empty-collect-module.spec.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { ZERO_ADDRESS } from '../../helpers/constants';
|
||||
import { ERRORS } from '../../helpers/errors';
|
||||
import {
|
||||
approvalFollowModule,
|
||||
emptyCollectModule,
|
||||
FIRST_PROFILE_ID,
|
||||
governance,
|
||||
lensHub,
|
||||
makeSuiteCleanRoom,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
MOCK_URI,
|
||||
user,
|
||||
userAddress,
|
||||
userTwo,
|
||||
userTwoAddress,
|
||||
} from '../../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Empty Collect Module', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
context('Collecting', function () {
|
||||
it('UserTwo should fail to collect without following without any follow module set', async function () {
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, [])).to.be.revertedWith(
|
||||
ERRORS.FOLLOW_INVALID
|
||||
);
|
||||
});
|
||||
|
||||
it('UserTwo should mirror the original post, fail to collect from their mirror without following the original profile', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, [])).to.be.revertedWith(
|
||||
ERRORS.FOLLOW_INVALID
|
||||
);
|
||||
});
|
||||
|
||||
it('UserTwo should mirror the original post, fail to collect from their mirror without following the original profile when it has a follow module set', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistFollowModule(approvalFollowModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.setFollowModule(FIRST_PROFILE_ID, approvalFollowModule.address, [])
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, [])).to.be.revertedWith(
|
||||
ERRORS.FOLLOW_INVALID
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('UserTwo should collect with success when following without any follow module set', async function () {
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, [])).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('UserTwo should collect with success when following according the follow module set', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistFollowModule(approvalFollowModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.setFollowModule(FIRST_PROFILE_ID, approvalFollowModule.address, [])
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
approvalFollowModule.connect(user).approve(FIRST_PROFILE_ID, [userTwoAddress], [true])
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, [])).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('UserTwo should mirror the original post, collect with success from their mirror when following the original profile which has not follow module set', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, [])).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('UserTwo should mirror the original post, collect with success from their mirror when following the original profile which has a follow module set', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistFollowModule(approvalFollowModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.setFollowModule(FIRST_PROFILE_ID, approvalFollowModule.address, [])
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
approvalFollowModule.connect(user).approve(FIRST_PROFILE_ID, [userTwoAddress], [true])
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, [])).to.not.be.reverted;
|
||||
});
|
||||
});
|
||||
});
|
||||
542
test/modules/collect/fee-collect-module.spec.ts
Normal file
542
test/modules/collect/fee-collect-module.spec.ts
Normal file
@@ -0,0 +1,542 @@
|
||||
import { BigNumber } from '@ethersproject/contracts/node_modules/@ethersproject/bignumber';
|
||||
import { parseEther } from '@ethersproject/units';
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { MAX_UINT256, ZERO_ADDRESS } from '../../helpers/constants';
|
||||
import { ERRORS } from '../../helpers/errors';
|
||||
import { getTimestamp, matchEvent, waitForTx } from '../../helpers/utils';
|
||||
import {
|
||||
abiCoder,
|
||||
BPS_MAX,
|
||||
currency,
|
||||
feeCollectModule,
|
||||
FIRST_PROFILE_ID,
|
||||
governance,
|
||||
lensHub,
|
||||
makeSuiteCleanRoom,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
MOCK_URI,
|
||||
moduleGlobals,
|
||||
REFERRAL_FEE_BPS,
|
||||
treasuryAddress,
|
||||
TREASURY_FEE_BPS,
|
||||
userAddress,
|
||||
userTwo,
|
||||
userTwoAddress,
|
||||
} from '../../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Fee Collect Module', function () {
|
||||
const DEFAULT_COLLECT_PRICE = parseEther('10');
|
||||
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(feeCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
moduleGlobals.connect(governance).whitelistCurrency(currency.address, true)
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
context('Publication Creation', function () {
|
||||
it('user should fail to post with fee collect module using unwhitelisted currency', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_PRICE, userTwoAddress, userAddress, REFERRAL_FEE_BPS]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: feeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('user should fail to post with fee collect module using zero recipient', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_PRICE, currency.address, ZERO_ADDRESS, REFERRAL_FEE_BPS]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: feeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('user should fail to post with fee collect module using referral fee greater than max BPS', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_PRICE, currency.address, userAddress, 10001]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: feeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('user should fail to post with fee collect module using amount lower than max BPS', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16'],
|
||||
[9999, currency.address, userAddress, REFERRAL_FEE_BPS]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: feeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
context('Collecting', function () {
|
||||
beforeEach(async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_PRICE, currency.address, userAddress, REFERRAL_FEE_BPS]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: feeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('UserTwo should fail to collect without following', async function () {
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(
|
||||
lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)
|
||||
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to collect passing a different expected price in data', async function () {
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE.div(2)]
|
||||
);
|
||||
await expect(
|
||||
lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)
|
||||
).to.be.revertedWith(ERRORS.MODULE_DATA_MISMATCH);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to collect passing a different expected currency in data', async function () {
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(['address', 'uint256'], [userAddress, DEFAULT_COLLECT_PRICE]);
|
||||
await expect(
|
||||
lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)
|
||||
).to.be.revertedWith(ERRORS.MODULE_DATA_MISMATCH);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to collect without first approving module with currency', async function () {
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(
|
||||
lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)
|
||||
).to.be.revertedWith(ERRORS.ERC20_TRANSFER_EXCEEDS_ALLOWANCE);
|
||||
});
|
||||
|
||||
it('UserTwo should mirror the original post, fail to collect from their mirror without following the original profile', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(['uint256'], [DEFAULT_COLLECT_PRICE]);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.be.revertedWith(
|
||||
ERRORS.FOLLOW_INVALID
|
||||
);
|
||||
});
|
||||
|
||||
it('UserTwo should mirror the original post, fail to collect from their mirror passing a different expected price in data', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE.div(2)]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.be.revertedWith(
|
||||
ERRORS.MODULE_DATA_MISMATCH
|
||||
);
|
||||
});
|
||||
|
||||
it('UserTwo should mirror the original post, fail to collect from their mirror passing a different expected currency in data', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(['address', 'uint256'], [userAddress, DEFAULT_COLLECT_PRICE]);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.be.revertedWith(
|
||||
ERRORS.MODULE_DATA_MISMATCH
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('User should post with fee collect module as the collect module and data, correct events should be emitted', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_PRICE, currency.address, userAddress, REFERRAL_FEE_BPS]
|
||||
);
|
||||
const tx = lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: feeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
});
|
||||
|
||||
const receipt = await waitForTx(tx);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'PostCreated', [
|
||||
FIRST_PROFILE_ID,
|
||||
1,
|
||||
MOCK_URI,
|
||||
feeCollectModule.address,
|
||||
[collectModuleData],
|
||||
ZERO_ADDRESS,
|
||||
[],
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('User should post with the fee collect module as the collect module and data, fetched publication data should be accurate', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_PRICE, currency.address, userAddress, REFERRAL_FEE_BPS]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: feeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
const postTimestamp = await getTimestamp();
|
||||
|
||||
const fetchedData = await feeCollectModule.getPublicationData(FIRST_PROFILE_ID, 1);
|
||||
expect(fetchedData.amount).to.eq(DEFAULT_COLLECT_PRICE);
|
||||
expect(fetchedData.recipient).to.eq(userAddress);
|
||||
expect(fetchedData.currency).to.eq(currency.address);
|
||||
expect(fetchedData.referralFee).to.eq(REFERRAL_FEE_BPS);
|
||||
});
|
||||
|
||||
it('User should post with the fee collect module as the collect module and data, user two follows, then collects and pays fee, fee distribution is valid', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_PRICE, currency.address, userAddress, REFERRAL_FEE_BPS]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: feeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
await expect(
|
||||
currency.connect(userTwo).approve(feeCollectModule.address, MAX_UINT256)
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)).to.not.be.reverted;
|
||||
|
||||
const expectedTreasuryAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.mul(TREASURY_FEE_BPS)
|
||||
.div(BPS_MAX);
|
||||
const expectedRecipientAmount =
|
||||
BigNumber.from(DEFAULT_COLLECT_PRICE).sub(expectedTreasuryAmount);
|
||||
|
||||
expect(await currency.balanceOf(userTwoAddress)).to.eq(
|
||||
BigNumber.from(MAX_UINT256).sub(DEFAULT_COLLECT_PRICE)
|
||||
);
|
||||
expect(await currency.balanceOf(userAddress)).to.eq(expectedRecipientAmount);
|
||||
expect(await currency.balanceOf(treasuryAddress)).to.eq(expectedTreasuryAmount);
|
||||
});
|
||||
|
||||
it('User should post with the fee collect module as the collect module and data, user two follows, then collects twice, fee distribution is valid', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_PRICE, currency.address, userAddress, REFERRAL_FEE_BPS]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: feeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
await expect(
|
||||
currency.connect(userTwo).approve(feeCollectModule.address, MAX_UINT256)
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)).to.not.be.reverted;
|
||||
|
||||
const expectedTreasuryAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.mul(TREASURY_FEE_BPS)
|
||||
.div(BPS_MAX);
|
||||
const expectedRecipientAmount =
|
||||
BigNumber.from(DEFAULT_COLLECT_PRICE).sub(expectedTreasuryAmount);
|
||||
|
||||
expect(await currency.balanceOf(userTwoAddress)).to.eq(
|
||||
BigNumber.from(MAX_UINT256).sub(BigNumber.from(DEFAULT_COLLECT_PRICE).mul(2))
|
||||
);
|
||||
expect(await currency.balanceOf(userAddress)).to.eq(expectedRecipientAmount.mul(2));
|
||||
expect(await currency.balanceOf(treasuryAddress)).to.eq(expectedTreasuryAmount.mul(2));
|
||||
});
|
||||
|
||||
it('User should post with the fee collect module as the collect module and data, user two mirrors, follows, then collects from their mirror and pays fee, fee distribution is valid', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_PRICE, currency.address, userAddress, REFERRAL_FEE_BPS]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: feeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
await expect(
|
||||
currency.connect(userTwo).approve(feeCollectModule.address, MAX_UINT256)
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.not.be.reverted;
|
||||
|
||||
const expectedTreasuryAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.mul(TREASURY_FEE_BPS)
|
||||
.div(BPS_MAX);
|
||||
const expectedReferralAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.sub(expectedTreasuryAmount)
|
||||
.mul(REFERRAL_FEE_BPS)
|
||||
.div(BPS_MAX);
|
||||
const expectedReferrerAmount = BigNumber.from(MAX_UINT256)
|
||||
.sub(DEFAULT_COLLECT_PRICE)
|
||||
.add(expectedReferralAmount);
|
||||
const expectedRecipientAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.sub(expectedTreasuryAmount)
|
||||
.sub(expectedReferralAmount);
|
||||
|
||||
expect(await currency.balanceOf(userTwoAddress)).to.eq(expectedReferrerAmount);
|
||||
expect(await currency.balanceOf(userAddress)).to.eq(expectedRecipientAmount);
|
||||
expect(await currency.balanceOf(treasuryAddress)).to.eq(expectedTreasuryAmount);
|
||||
});
|
||||
|
||||
it('User should post with the fee collect module as the collect module and data, with no referral fee, user two mirrors, follows, then collects from their mirror and pays fee, fee distribution is valid', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_PRICE, currency.address, userAddress, 0]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: feeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
await expect(
|
||||
currency.connect(userTwo).approve(feeCollectModule.address, MAX_UINT256)
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.not.be.reverted;
|
||||
|
||||
const expectedTreasuryAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.mul(TREASURY_FEE_BPS)
|
||||
.div(BPS_MAX);
|
||||
const expectedRecipientAmount =
|
||||
BigNumber.from(DEFAULT_COLLECT_PRICE).sub(expectedTreasuryAmount);
|
||||
|
||||
expect(await currency.balanceOf(userTwoAddress)).to.eq(
|
||||
BigNumber.from(MAX_UINT256).sub(DEFAULT_COLLECT_PRICE)
|
||||
);
|
||||
expect(await currency.balanceOf(userAddress)).to.eq(expectedRecipientAmount);
|
||||
expect(await currency.balanceOf(treasuryAddress)).to.eq(expectedTreasuryAmount);
|
||||
});
|
||||
});
|
||||
});
|
||||
673
test/modules/collect/limited-fee-collect-module.spec.ts
Normal file
673
test/modules/collect/limited-fee-collect-module.spec.ts
Normal file
@@ -0,0 +1,673 @@
|
||||
import { BigNumber } from '@ethersproject/contracts/node_modules/@ethersproject/bignumber';
|
||||
import { parseEther } from '@ethersproject/units';
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { MAX_UINT256, ZERO_ADDRESS } from '../../helpers/constants';
|
||||
import { ERRORS } from '../../helpers/errors';
|
||||
import { getTimestamp, matchEvent, waitForTx } from '../../helpers/utils';
|
||||
import {
|
||||
abiCoder,
|
||||
BPS_MAX,
|
||||
currency,
|
||||
FIRST_PROFILE_ID,
|
||||
governance,
|
||||
lensHub,
|
||||
limitedFeeCollectModule,
|
||||
makeSuiteCleanRoom,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
MOCK_URI,
|
||||
moduleGlobals,
|
||||
REFERRAL_FEE_BPS,
|
||||
treasuryAddress,
|
||||
TREASURY_FEE_BPS,
|
||||
userAddress,
|
||||
userTwo,
|
||||
userTwoAddress,
|
||||
} from '../../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Limited Fee Collect Module', function () {
|
||||
const DEFAULT_COLLECT_PRICE = parseEther('10');
|
||||
const DEFAULT_COLLECT_LIMIT = 3;
|
||||
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(limitedFeeCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
moduleGlobals.connect(governance).whitelistCurrency(currency.address, true)
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
context('Publication Creation', function () {
|
||||
it('user should fail to post with limited fee collect module using zero collect limit', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[0, DEFAULT_COLLECT_PRICE, currency.address, userAddress, REFERRAL_FEE_BPS]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('user should fail to post with limited fee collect module using unwhitelisted currency', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[
|
||||
DEFAULT_COLLECT_LIMIT,
|
||||
DEFAULT_COLLECT_PRICE,
|
||||
userTwoAddress,
|
||||
userAddress,
|
||||
REFERRAL_FEE_BPS,
|
||||
]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('user should fail to post with limited fee collect module using zero recipient', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[
|
||||
DEFAULT_COLLECT_LIMIT,
|
||||
DEFAULT_COLLECT_PRICE,
|
||||
currency.address,
|
||||
ZERO_ADDRESS,
|
||||
REFERRAL_FEE_BPS,
|
||||
]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('user should fail to post with limited fee collect module using referral fee greater than max BPS', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_LIMIT, DEFAULT_COLLECT_PRICE, currency.address, userAddress, 10001]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('user should fail to post with limited fee collect module using amount lower than max BPS', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_LIMIT, 9999, currency.address, userAddress, REFERRAL_FEE_BPS]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
context('Collecting', function () {
|
||||
beforeEach(async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[
|
||||
DEFAULT_COLLECT_LIMIT,
|
||||
DEFAULT_COLLECT_PRICE,
|
||||
currency.address,
|
||||
userAddress,
|
||||
REFERRAL_FEE_BPS,
|
||||
]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('UserTwo should fail to collect without following', async function () {
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(
|
||||
lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)
|
||||
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to collect passing a different expected price in data', async function () {
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE.div(2)]
|
||||
);
|
||||
await expect(
|
||||
lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)
|
||||
).to.be.revertedWith(ERRORS.MODULE_DATA_MISMATCH);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to collect passing a different expected currency in data', async function () {
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(['address', 'uint256'], [userAddress, DEFAULT_COLLECT_PRICE]);
|
||||
await expect(
|
||||
lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)
|
||||
).to.be.revertedWith(ERRORS.MODULE_DATA_MISMATCH);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to collect without first approving module with currency', async function () {
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(
|
||||
lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)
|
||||
).to.be.revertedWith(ERRORS.ERC20_TRANSFER_EXCEEDS_ALLOWANCE);
|
||||
});
|
||||
|
||||
it('UserTwo should mirror the original post, fail to collect from their mirror without following the original profile', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.be.revertedWith(
|
||||
ERRORS.FOLLOW_INVALID
|
||||
);
|
||||
});
|
||||
|
||||
it('UserTwo should mirror the original post, fail to collect from their mirror passing a different expected price in data', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE.div(2)]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.be.revertedWith(
|
||||
ERRORS.MODULE_DATA_MISMATCH
|
||||
);
|
||||
});
|
||||
|
||||
it('UserTwo should mirror the original post, fail to collect from their mirror passing a different expected currency in data', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const data = abiCoder.encode(['address', 'uint256'], [userAddress, DEFAULT_COLLECT_PRICE]);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.be.revertedWith(
|
||||
ERRORS.MODULE_DATA_MISMATCH
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('User should post with limited fee collect module as the collect module and data, correct events should be emitted', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[
|
||||
DEFAULT_COLLECT_LIMIT,
|
||||
DEFAULT_COLLECT_PRICE,
|
||||
currency.address,
|
||||
userAddress,
|
||||
REFERRAL_FEE_BPS,
|
||||
]
|
||||
);
|
||||
const tx = lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
});
|
||||
|
||||
const receipt = await waitForTx(tx);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'PostCreated', [
|
||||
FIRST_PROFILE_ID,
|
||||
1,
|
||||
MOCK_URI,
|
||||
limitedFeeCollectModule.address,
|
||||
collectModuleData,
|
||||
ZERO_ADDRESS,
|
||||
[],
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('User should post with limited fee collect module as the collect module and data, fetched publication data should be accurate', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[
|
||||
DEFAULT_COLLECT_LIMIT,
|
||||
DEFAULT_COLLECT_PRICE,
|
||||
currency.address,
|
||||
userAddress,
|
||||
REFERRAL_FEE_BPS,
|
||||
]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const fetchedData = await limitedFeeCollectModule.getPublicationData(FIRST_PROFILE_ID, 1);
|
||||
expect(fetchedData.collectLimit).to.eq(DEFAULT_COLLECT_LIMIT);
|
||||
expect(fetchedData.amount).to.eq(DEFAULT_COLLECT_PRICE);
|
||||
expect(fetchedData.recipient).to.eq(userAddress);
|
||||
expect(fetchedData.currency).to.eq(currency.address);
|
||||
expect(fetchedData.referralFee).to.eq(REFERRAL_FEE_BPS);
|
||||
});
|
||||
|
||||
it('User should post with limited fee collect module as the collect module and data, user two follows, then collects and pays fee, fee distribution is valid', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[
|
||||
DEFAULT_COLLECT_LIMIT,
|
||||
DEFAULT_COLLECT_PRICE,
|
||||
currency.address,
|
||||
userAddress,
|
||||
REFERRAL_FEE_BPS,
|
||||
]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
await expect(
|
||||
currency.connect(userTwo).approve(limitedFeeCollectModule.address, MAX_UINT256)
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)).to.not.be.reverted;
|
||||
|
||||
const expectedTreasuryAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.mul(TREASURY_FEE_BPS)
|
||||
.div(BPS_MAX);
|
||||
const expectedRecipientAmount =
|
||||
BigNumber.from(DEFAULT_COLLECT_PRICE).sub(expectedTreasuryAmount);
|
||||
|
||||
expect(await currency.balanceOf(userTwoAddress)).to.eq(
|
||||
BigNumber.from(MAX_UINT256).sub(DEFAULT_COLLECT_PRICE)
|
||||
);
|
||||
expect(await currency.balanceOf(userAddress)).to.eq(expectedRecipientAmount);
|
||||
expect(await currency.balanceOf(treasuryAddress)).to.eq(expectedTreasuryAmount);
|
||||
});
|
||||
|
||||
it('User should post with limited fee collect module as the collect module and data, user two follows, then collects twice, fee distribution is valid', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[
|
||||
DEFAULT_COLLECT_LIMIT,
|
||||
DEFAULT_COLLECT_PRICE,
|
||||
currency.address,
|
||||
userAddress,
|
||||
REFERRAL_FEE_BPS,
|
||||
]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
await expect(
|
||||
currency.connect(userTwo).approve(limitedFeeCollectModule.address, MAX_UINT256)
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)).to.not.be.reverted;
|
||||
|
||||
const expectedTreasuryAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.mul(TREASURY_FEE_BPS)
|
||||
.div(BPS_MAX);
|
||||
const expectedRecipientAmount =
|
||||
BigNumber.from(DEFAULT_COLLECT_PRICE).sub(expectedTreasuryAmount);
|
||||
|
||||
expect(await currency.balanceOf(userTwoAddress)).to.eq(
|
||||
BigNumber.from(MAX_UINT256).sub(BigNumber.from(DEFAULT_COLLECT_PRICE).mul(2))
|
||||
);
|
||||
expect(await currency.balanceOf(userAddress)).to.eq(expectedRecipientAmount.mul(2));
|
||||
expect(await currency.balanceOf(treasuryAddress)).to.eq(expectedTreasuryAmount.mul(2));
|
||||
});
|
||||
|
||||
it('User should post with limited fee collect module as the collect module and data, user two mirrors, follows, then collects from their mirror and pays fee, fee distribution is valid', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[
|
||||
DEFAULT_COLLECT_LIMIT,
|
||||
DEFAULT_COLLECT_PRICE,
|
||||
currency.address,
|
||||
userAddress,
|
||||
REFERRAL_FEE_BPS,
|
||||
]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
await expect(
|
||||
currency.connect(userTwo).approve(limitedFeeCollectModule.address, MAX_UINT256)
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.not.be.reverted;
|
||||
|
||||
const expectedTreasuryAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.mul(TREASURY_FEE_BPS)
|
||||
.div(BPS_MAX);
|
||||
const expectedReferralAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.sub(expectedTreasuryAmount)
|
||||
.mul(REFERRAL_FEE_BPS)
|
||||
.div(BPS_MAX);
|
||||
const expectedReferrerAmount = BigNumber.from(MAX_UINT256)
|
||||
.sub(DEFAULT_COLLECT_PRICE)
|
||||
.add(expectedReferralAmount);
|
||||
const expectedRecipientAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.sub(expectedTreasuryAmount)
|
||||
.sub(expectedReferralAmount);
|
||||
|
||||
expect(await currency.balanceOf(userTwoAddress)).to.eq(expectedReferrerAmount);
|
||||
expect(await currency.balanceOf(userAddress)).to.eq(expectedRecipientAmount);
|
||||
expect(await currency.balanceOf(treasuryAddress)).to.eq(expectedTreasuryAmount);
|
||||
});
|
||||
|
||||
it('User should post with limited fee collect module as the collect module and data, with no referral fee, user two mirrors, follows, then collects from their mirror and pays fee, fee distribution is valid', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_LIMIT, DEFAULT_COLLECT_PRICE, currency.address, userAddress, 0]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
await expect(
|
||||
currency.connect(userTwo).approve(limitedFeeCollectModule.address, MAX_UINT256)
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.not.be.reverted;
|
||||
|
||||
const expectedTreasuryAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.mul(TREASURY_FEE_BPS)
|
||||
.div(BPS_MAX);
|
||||
const expectedRecipientAmount =
|
||||
BigNumber.from(DEFAULT_COLLECT_PRICE).sub(expectedTreasuryAmount);
|
||||
|
||||
expect(await currency.balanceOf(userTwoAddress)).to.eq(
|
||||
BigNumber.from(MAX_UINT256).sub(DEFAULT_COLLECT_PRICE)
|
||||
);
|
||||
expect(await currency.balanceOf(userAddress)).to.eq(expectedRecipientAmount);
|
||||
expect(await currency.balanceOf(treasuryAddress)).to.eq(expectedTreasuryAmount);
|
||||
});
|
||||
|
||||
it('User should post with limited fee collect module as the collect module and data, user two mirrors, follows, then collects once from the original, twice from the mirror, and fails to collect a third time from either the mirror or the original', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[
|
||||
DEFAULT_COLLECT_LIMIT,
|
||||
DEFAULT_COLLECT_PRICE,
|
||||
currency.address,
|
||||
userAddress,
|
||||
REFERRAL_FEE_BPS,
|
||||
]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
await expect(
|
||||
currency.connect(userTwo).approve(limitedFeeCollectModule.address, MAX_UINT256)
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)).to.be.revertedWith(
|
||||
ERRORS.MINT_LIMIT_EXCEEDED
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.be.revertedWith(
|
||||
ERRORS.MINT_LIMIT_EXCEEDED
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
745
test/modules/collect/limited-timed-fee-collect-module.spec.ts
Normal file
745
test/modules/collect/limited-timed-fee-collect-module.spec.ts
Normal file
@@ -0,0 +1,745 @@
|
||||
import { BigNumber } from '@ethersproject/contracts/node_modules/@ethersproject/bignumber';
|
||||
import { parseEther } from '@ethersproject/units';
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { MAX_UINT256, ZERO_ADDRESS } from '../../helpers/constants';
|
||||
import { ERRORS } from '../../helpers/errors';
|
||||
import { getTimestamp, matchEvent, setNextBlockTimestamp, waitForTx } from '../../helpers/utils';
|
||||
import {
|
||||
abiCoder,
|
||||
BPS_MAX,
|
||||
currency,
|
||||
FIRST_PROFILE_ID,
|
||||
governance,
|
||||
lensHub,
|
||||
limitedTimedFeeCollectModule,
|
||||
makeSuiteCleanRoom,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
MOCK_URI,
|
||||
moduleGlobals,
|
||||
REFERRAL_FEE_BPS,
|
||||
treasuryAddress,
|
||||
TREASURY_FEE_BPS,
|
||||
userAddress,
|
||||
userTwo,
|
||||
userTwoAddress,
|
||||
} from '../../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Limited Timed Fee Collect Module', function () {
|
||||
const DEFAULT_COLLECT_PRICE = parseEther('10');
|
||||
const DEFAULT_COLLECT_LIMIT = 3;
|
||||
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(limitedTimedFeeCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
moduleGlobals.connect(governance).whitelistCurrency(currency.address, true)
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
context('Publication Creation', function () {
|
||||
it('user should fail to post with limited timed fee collect module using zero collect limit', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[0, DEFAULT_COLLECT_PRICE, currency.address, userAddress, REFERRAL_FEE_BPS]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedTimedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('user should fail to post with limited timed fee collect module using unwhitelisted currency', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[
|
||||
DEFAULT_COLLECT_LIMIT,
|
||||
DEFAULT_COLLECT_PRICE,
|
||||
userTwoAddress,
|
||||
userAddress,
|
||||
REFERRAL_FEE_BPS,
|
||||
]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedTimedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('user should fail to post with limited timed fee collect module using zero recipient', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[
|
||||
DEFAULT_COLLECT_LIMIT,
|
||||
DEFAULT_COLLECT_PRICE,
|
||||
currency.address,
|
||||
ZERO_ADDRESS,
|
||||
REFERRAL_FEE_BPS,
|
||||
]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedTimedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('user should fail to post with limited timed fee collect module using referral fee greater than max BPS', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_LIMIT, DEFAULT_COLLECT_PRICE, currency.address, userAddress, 10001]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedTimedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('user should fail to post with limited timed fee collect module using amount lower than max BPS', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_LIMIT, 9999, currency.address, userAddress, REFERRAL_FEE_BPS]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedTimedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
context('Collecting', function () {
|
||||
beforeEach(async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[
|
||||
DEFAULT_COLLECT_LIMIT,
|
||||
DEFAULT_COLLECT_PRICE,
|
||||
currency.address,
|
||||
userAddress,
|
||||
REFERRAL_FEE_BPS,
|
||||
]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedTimedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('UserTwo should fail to collect without following', async function () {
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(
|
||||
lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)
|
||||
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to collect after the collect end timestmap', async function () {
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
const currentTimestamp = await getTimestamp();
|
||||
await setNextBlockTimestamp(Number(currentTimestamp) + 24 * 60 * 60);
|
||||
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(
|
||||
lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)
|
||||
).to.be.revertedWith(ERRORS.COLLECT_EXPIRED);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to collect passing a different expected price in data', async function () {
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE.div(2)]
|
||||
);
|
||||
await expect(
|
||||
lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)
|
||||
).to.be.revertedWith(ERRORS.MODULE_DATA_MISMATCH);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to collect passing a different expected currency in data', async function () {
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(['address', 'uint256'], [userAddress, DEFAULT_COLLECT_PRICE]);
|
||||
await expect(
|
||||
lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)
|
||||
).to.be.revertedWith(ERRORS.MODULE_DATA_MISMATCH);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to collect without first approving module with currency', async function () {
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(
|
||||
lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)
|
||||
).to.be.revertedWith(ERRORS.ERC20_TRANSFER_EXCEEDS_ALLOWANCE);
|
||||
});
|
||||
|
||||
it('UserTwo should mirror the original post, fail to collect from their mirror without following the original profile', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.be.revertedWith(
|
||||
ERRORS.FOLLOW_INVALID
|
||||
);
|
||||
});
|
||||
|
||||
it('UserTwo should mirror the original post, fail to collect from their mirror after the collect end timestamp', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
const currentTimestamp = await getTimestamp();
|
||||
await setNextBlockTimestamp(Number(currentTimestamp) + 24 * 60 * 60);
|
||||
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.be.revertedWith(
|
||||
ERRORS.COLLECT_EXPIRED
|
||||
);
|
||||
});
|
||||
|
||||
it('UserTwo should mirror the original post, fail to collect from their mirror passing a different expected price in data', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE.div(2)]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.be.revertedWith(
|
||||
ERRORS.MODULE_DATA_MISMATCH
|
||||
);
|
||||
});
|
||||
|
||||
it('UserTwo should mirror the original post, fail to collect from their mirror passing a different expected currency in data', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(['address', 'uint256'], [userAddress, DEFAULT_COLLECT_PRICE]);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.be.revertedWith(
|
||||
ERRORS.MODULE_DATA_MISMATCH
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('User should post with limited timed fee collect module as the collect module and data, correct events should be emitted', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[
|
||||
DEFAULT_COLLECT_LIMIT,
|
||||
DEFAULT_COLLECT_PRICE,
|
||||
currency.address,
|
||||
userAddress,
|
||||
REFERRAL_FEE_BPS,
|
||||
]
|
||||
);
|
||||
const tx = lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedTimedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
});
|
||||
|
||||
const receipt = await waitForTx(tx);
|
||||
|
||||
const postTimestamp = await getTimestamp();
|
||||
const endTimestamp = BigNumber.from(postTimestamp).add(24 * 60 * 60);
|
||||
const expectedData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16', 'uint40'],
|
||||
[
|
||||
DEFAULT_COLLECT_LIMIT,
|
||||
DEFAULT_COLLECT_PRICE,
|
||||
currency.address,
|
||||
userAddress,
|
||||
REFERRAL_FEE_BPS,
|
||||
endTimestamp,
|
||||
]
|
||||
);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'PostCreated', [
|
||||
FIRST_PROFILE_ID,
|
||||
1,
|
||||
MOCK_URI,
|
||||
limitedTimedFeeCollectModule.address,
|
||||
expectedData,
|
||||
ZERO_ADDRESS,
|
||||
[],
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('User should post with limited timed fee collect module as the collect module and data, fetched publication data should be accurate', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[
|
||||
DEFAULT_COLLECT_LIMIT,
|
||||
DEFAULT_COLLECT_PRICE,
|
||||
currency.address,
|
||||
userAddress,
|
||||
REFERRAL_FEE_BPS,
|
||||
]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedTimedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
const postTimestamp = await getTimestamp();
|
||||
|
||||
const fetchedData = await limitedTimedFeeCollectModule.getPublicationData(
|
||||
FIRST_PROFILE_ID,
|
||||
1
|
||||
);
|
||||
expect(fetchedData.collectLimit).to.eq(DEFAULT_COLLECT_LIMIT);
|
||||
expect(fetchedData.amount).to.eq(DEFAULT_COLLECT_PRICE);
|
||||
expect(fetchedData.recipient).to.eq(userAddress);
|
||||
expect(fetchedData.currency).to.eq(currency.address);
|
||||
expect(fetchedData.referralFee).to.eq(REFERRAL_FEE_BPS);
|
||||
expect(fetchedData.endTimestamp).to.eq(BigNumber.from(postTimestamp).add(24 * 60 * 60));
|
||||
});
|
||||
|
||||
it('User should post with limited timed fee collect module as the collect module and data, user two follows, then collects and pays fee, fee distribution is valid', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[
|
||||
DEFAULT_COLLECT_LIMIT,
|
||||
DEFAULT_COLLECT_PRICE,
|
||||
currency.address,
|
||||
userAddress,
|
||||
REFERRAL_FEE_BPS,
|
||||
]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedTimedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
await expect(
|
||||
currency.connect(userTwo).approve(limitedTimedFeeCollectModule.address, MAX_UINT256)
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)).to.not.be.reverted;
|
||||
|
||||
const expectedTreasuryAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.mul(TREASURY_FEE_BPS)
|
||||
.div(BPS_MAX);
|
||||
const expectedRecipientAmount =
|
||||
BigNumber.from(DEFAULT_COLLECT_PRICE).sub(expectedTreasuryAmount);
|
||||
|
||||
expect(await currency.balanceOf(userTwoAddress)).to.eq(
|
||||
BigNumber.from(MAX_UINT256).sub(DEFAULT_COLLECT_PRICE)
|
||||
);
|
||||
expect(await currency.balanceOf(userAddress)).to.eq(expectedRecipientAmount);
|
||||
expect(await currency.balanceOf(treasuryAddress)).to.eq(expectedTreasuryAmount);
|
||||
});
|
||||
|
||||
it('User should post with limited timed fee collect module as the collect module and data, user two follows, then collects twice, fee distribution is valid', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[
|
||||
DEFAULT_COLLECT_LIMIT,
|
||||
DEFAULT_COLLECT_PRICE,
|
||||
currency.address,
|
||||
userAddress,
|
||||
REFERRAL_FEE_BPS,
|
||||
]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedTimedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
await expect(
|
||||
currency.connect(userTwo).approve(limitedTimedFeeCollectModule.address, MAX_UINT256)
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)).to.not.be.reverted;
|
||||
|
||||
const expectedTreasuryAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.mul(TREASURY_FEE_BPS)
|
||||
.div(BPS_MAX);
|
||||
const expectedRecipientAmount =
|
||||
BigNumber.from(DEFAULT_COLLECT_PRICE).sub(expectedTreasuryAmount);
|
||||
|
||||
expect(await currency.balanceOf(userTwoAddress)).to.eq(
|
||||
BigNumber.from(MAX_UINT256).sub(BigNumber.from(DEFAULT_COLLECT_PRICE).mul(2))
|
||||
);
|
||||
expect(await currency.balanceOf(userAddress)).to.eq(expectedRecipientAmount.mul(2));
|
||||
expect(await currency.balanceOf(treasuryAddress)).to.eq(expectedTreasuryAmount.mul(2));
|
||||
});
|
||||
|
||||
it('User should post with limited timed fee collect module as the collect module and data, user two mirrors, follows, then collects from their mirror and pays fee, fee distribution is valid', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[
|
||||
DEFAULT_COLLECT_LIMIT,
|
||||
DEFAULT_COLLECT_PRICE,
|
||||
currency.address,
|
||||
userAddress,
|
||||
REFERRAL_FEE_BPS,
|
||||
]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedTimedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
await expect(
|
||||
currency.connect(userTwo).approve(limitedTimedFeeCollectModule.address, MAX_UINT256)
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.not.be.reverted;
|
||||
|
||||
const expectedTreasuryAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.mul(TREASURY_FEE_BPS)
|
||||
.div(BPS_MAX);
|
||||
const expectedReferralAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.sub(expectedTreasuryAmount)
|
||||
.mul(REFERRAL_FEE_BPS)
|
||||
.div(BPS_MAX);
|
||||
const expectedReferrerAmount = BigNumber.from(MAX_UINT256)
|
||||
.sub(DEFAULT_COLLECT_PRICE)
|
||||
.add(expectedReferralAmount);
|
||||
const expectedRecipientAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.sub(expectedTreasuryAmount)
|
||||
.sub(expectedReferralAmount);
|
||||
|
||||
expect(await currency.balanceOf(userTwoAddress)).to.eq(expectedReferrerAmount);
|
||||
expect(await currency.balanceOf(userAddress)).to.eq(expectedRecipientAmount);
|
||||
expect(await currency.balanceOf(treasuryAddress)).to.eq(expectedTreasuryAmount);
|
||||
});
|
||||
|
||||
it('User should post with limited timed fee collect module as the collect module and data, with no referral fee, user two mirrors, follows, then collects from their mirror and pays fee, fee distribution is valid', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_LIMIT, DEFAULT_COLLECT_PRICE, currency.address, userAddress, 0]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedTimedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
await expect(
|
||||
currency.connect(userTwo).approve(limitedTimedFeeCollectModule.address, MAX_UINT256)
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.not.be.reverted;
|
||||
|
||||
const expectedTreasuryAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.mul(TREASURY_FEE_BPS)
|
||||
.div(BPS_MAX);
|
||||
const expectedRecipientAmount =
|
||||
BigNumber.from(DEFAULT_COLLECT_PRICE).sub(expectedTreasuryAmount);
|
||||
|
||||
expect(await currency.balanceOf(userTwoAddress)).to.eq(
|
||||
BigNumber.from(MAX_UINT256).sub(DEFAULT_COLLECT_PRICE)
|
||||
);
|
||||
expect(await currency.balanceOf(userAddress)).to.eq(expectedRecipientAmount);
|
||||
expect(await currency.balanceOf(treasuryAddress)).to.eq(expectedTreasuryAmount);
|
||||
});
|
||||
|
||||
it('User should post with limited timed fee collect module as the collect module and data, user two mirrors, follows, then collects once from the original, twice from the mirror, and fails to collect a third time from either the mirror or the original', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'uint256', 'address', 'address', 'uint16'],
|
||||
[
|
||||
DEFAULT_COLLECT_LIMIT,
|
||||
DEFAULT_COLLECT_PRICE,
|
||||
currency.address,
|
||||
userAddress,
|
||||
REFERRAL_FEE_BPS,
|
||||
]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: limitedTimedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
await expect(
|
||||
currency.connect(userTwo).approve(limitedTimedFeeCollectModule.address, MAX_UINT256)
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)).to.be.revertedWith(
|
||||
ERRORS.MINT_LIMIT_EXCEEDED
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.be.revertedWith(
|
||||
ERRORS.MINT_LIMIT_EXCEEDED
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
116
test/modules/collect/revert-collect-module.spec.ts
Normal file
116
test/modules/collect/revert-collect-module.spec.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { ZERO_ADDRESS } from '../../helpers/constants';
|
||||
import { ERRORS } from '../../helpers/errors';
|
||||
import {
|
||||
FIRST_PROFILE_ID,
|
||||
governance,
|
||||
lensHub,
|
||||
makeSuiteCleanRoom,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
MOCK_URI,
|
||||
revertCollectModule,
|
||||
userAddress,
|
||||
userTwo,
|
||||
userTwoAddress,
|
||||
} from '../../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Revert Collect Module', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(revertCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: revertCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Collecting', function () {
|
||||
it('UserTwo should fail to collect without following', async function () {
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, [])).to.be.revertedWith(
|
||||
ERRORS.COLLECT_NOT_ALLOWED
|
||||
);
|
||||
});
|
||||
|
||||
it('UserTwo should mirror the original post, fail to collect from their mirror without following the original profile', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, [])).to.be.revertedWith(
|
||||
ERRORS.COLLECT_NOT_ALLOWED
|
||||
);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to collect while following', async function () {
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, [])).to.be.revertedWith(
|
||||
ERRORS.COLLECT_NOT_ALLOWED
|
||||
);
|
||||
});
|
||||
|
||||
it('UserTwo should mirror the original post, fail to collect from their mirror while following the original profile', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, [])).to.be.revertedWith(
|
||||
ERRORS.COLLECT_NOT_ALLOWED
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
605
test/modules/collect/timed-fee-collect-module.spec.ts
Normal file
605
test/modules/collect/timed-fee-collect-module.spec.ts
Normal file
@@ -0,0 +1,605 @@
|
||||
import { BigNumber } from '@ethersproject/contracts/node_modules/@ethersproject/bignumber';
|
||||
import { parseEther } from '@ethersproject/units';
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { MAX_UINT256, ZERO_ADDRESS } from '../../helpers/constants';
|
||||
import { ERRORS } from '../../helpers/errors';
|
||||
import { getTimestamp, matchEvent, setNextBlockTimestamp, waitForTx } from '../../helpers/utils';
|
||||
import {
|
||||
abiCoder,
|
||||
BPS_MAX,
|
||||
currency,
|
||||
FIRST_PROFILE_ID,
|
||||
governance,
|
||||
lensHub,
|
||||
makeSuiteCleanRoom,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
MOCK_URI,
|
||||
moduleGlobals,
|
||||
REFERRAL_FEE_BPS,
|
||||
timedFeeCollectModule,
|
||||
treasuryAddress,
|
||||
TREASURY_FEE_BPS,
|
||||
userAddress,
|
||||
userTwo,
|
||||
userTwoAddress,
|
||||
} from '../../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Timed Fee Collect Module', function () {
|
||||
const DEFAULT_COLLECT_PRICE = parseEther('10');
|
||||
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(timedFeeCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
moduleGlobals.connect(governance).whitelistCurrency(currency.address, true)
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
context('Publication Creation', function () {
|
||||
it('user should fail to post with timed fee collect module using unwhitelisted currency', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_PRICE, userTwoAddress, userAddress, REFERRAL_FEE_BPS]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: timedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('user should fail to post with timed fee collect module using zero recipient', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_PRICE, currency.address, ZERO_ADDRESS, REFERRAL_FEE_BPS]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: timedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('user should fail to post with timed fee collect module using referral fee greater than max BPS', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_PRICE, currency.address, userAddress, 10001]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: timedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('user should fail to post with timed fee collect module using amount lower than max BPS', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16'],
|
||||
[9999, currency.address, userAddress, REFERRAL_FEE_BPS]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: timedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
context('Collecting', function () {
|
||||
beforeEach(async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_PRICE, currency.address, userAddress, REFERRAL_FEE_BPS]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: timedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('UserTwo should fail to collect without following', async function () {
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(
|
||||
lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)
|
||||
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to collect after the collect end timestmap', async function () {
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
const currentTimestamp = await getTimestamp();
|
||||
await setNextBlockTimestamp(Number(currentTimestamp) + 24 * 60 * 60);
|
||||
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(
|
||||
lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)
|
||||
).to.be.revertedWith(ERRORS.COLLECT_EXPIRED);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to collect passing a different expected price in data', async function () {
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE.div(2)]
|
||||
);
|
||||
await expect(
|
||||
lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)
|
||||
).to.be.revertedWith(ERRORS.MODULE_DATA_MISMATCH);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to collect passing a different expected currency in data', async function () {
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(['address', 'uint256'], [userAddress, DEFAULT_COLLECT_PRICE]);
|
||||
await expect(
|
||||
lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)
|
||||
).to.be.revertedWith(ERRORS.MODULE_DATA_MISMATCH);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to collect without first approving module with currency', async function () {
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(
|
||||
lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)
|
||||
).to.be.revertedWith(ERRORS.ERC20_TRANSFER_EXCEEDS_ALLOWANCE);
|
||||
});
|
||||
|
||||
it('UserTwo should mirror the original post, fail to collect from their mirror without following the original profile', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.be.revertedWith(
|
||||
ERRORS.FOLLOW_INVALID
|
||||
);
|
||||
});
|
||||
|
||||
it('UserTwo should mirror the original post, fail to collect from their mirror after the collect end timestamp', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
const currentTimestamp = await getTimestamp();
|
||||
await setNextBlockTimestamp(Number(currentTimestamp) + 24 * 60 * 60);
|
||||
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.be.revertedWith(
|
||||
ERRORS.COLLECT_EXPIRED
|
||||
);
|
||||
});
|
||||
|
||||
it('UserTwo should mirror the original post, fail to collect from their mirror passing a different expected price in data', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE.div(2)]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.be.revertedWith(
|
||||
ERRORS.MODULE_DATA_MISMATCH
|
||||
);
|
||||
});
|
||||
|
||||
it('UserTwo should mirror the original post, fail to collect from their mirror passing a different expected currency in data', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(['address', 'uint256'], [userAddress, DEFAULT_COLLECT_PRICE]);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.be.revertedWith(
|
||||
ERRORS.MODULE_DATA_MISMATCH
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('User should post with timed fee collect module as the collect module and data, correct events should be emitted', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_PRICE, currency.address, userAddress, REFERRAL_FEE_BPS]
|
||||
);
|
||||
|
||||
const tx = lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: timedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
});
|
||||
|
||||
const receipt = await waitForTx(tx);
|
||||
|
||||
const postTimestamp = await getTimestamp();
|
||||
const endTimestamp = BigNumber.from(postTimestamp).add(24 * 60 * 60);
|
||||
const expectedData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16', 'uint40'],
|
||||
[DEFAULT_COLLECT_PRICE, currency.address, userAddress, REFERRAL_FEE_BPS, endTimestamp]
|
||||
);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'PostCreated', [
|
||||
FIRST_PROFILE_ID,
|
||||
1,
|
||||
MOCK_URI,
|
||||
timedFeeCollectModule.address,
|
||||
expectedData,
|
||||
ZERO_ADDRESS,
|
||||
[],
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('User should post with timed fee collect module as the collect module and data, fetched publication data should be accurate', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_PRICE, currency.address, userAddress, REFERRAL_FEE_BPS]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: timedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
const postTimestamp = await getTimestamp();
|
||||
|
||||
const fetchedData = await timedFeeCollectModule.getPublicationData(FIRST_PROFILE_ID, 1);
|
||||
expect(fetchedData.amount).to.eq(DEFAULT_COLLECT_PRICE);
|
||||
expect(fetchedData.recipient).to.eq(userAddress);
|
||||
expect(fetchedData.currency).to.eq(currency.address);
|
||||
expect(fetchedData.referralFee).to.eq(REFERRAL_FEE_BPS);
|
||||
expect(fetchedData.endTimestamp).to.eq(BigNumber.from(postTimestamp).add(24 * 60 * 60));
|
||||
});
|
||||
|
||||
it('User should post with timed fee collect module as the collect module and data, user two follows, then collects and pays fee, fee distribution is valid', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_PRICE, currency.address, userAddress, REFERRAL_FEE_BPS]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: timedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
await expect(
|
||||
currency.connect(userTwo).approve(timedFeeCollectModule.address, MAX_UINT256)
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)).to.not.be.reverted;
|
||||
|
||||
const expectedTreasuryAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.mul(TREASURY_FEE_BPS)
|
||||
.div(BPS_MAX);
|
||||
const expectedRecipientAmount =
|
||||
BigNumber.from(DEFAULT_COLLECT_PRICE).sub(expectedTreasuryAmount);
|
||||
|
||||
expect(await currency.balanceOf(userTwoAddress)).to.eq(
|
||||
BigNumber.from(MAX_UINT256).sub(DEFAULT_COLLECT_PRICE)
|
||||
);
|
||||
expect(await currency.balanceOf(userAddress)).to.eq(expectedRecipientAmount);
|
||||
expect(await currency.balanceOf(treasuryAddress)).to.eq(expectedTreasuryAmount);
|
||||
});
|
||||
|
||||
it('User should post with timed fee collect module as the collect module and data, user two follows, then collects twice, fee distribution is valid', async function () {
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_PRICE, currency.address, userAddress, REFERRAL_FEE_BPS]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: timedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
await expect(
|
||||
currency.connect(userTwo).approve(timedFeeCollectModule.address, MAX_UINT256)
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)).to.not.be.reverted;
|
||||
|
||||
const expectedTreasuryAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.mul(TREASURY_FEE_BPS)
|
||||
.div(BPS_MAX);
|
||||
const expectedRecipientAmount =
|
||||
BigNumber.from(DEFAULT_COLLECT_PRICE).sub(expectedTreasuryAmount);
|
||||
|
||||
expect(await currency.balanceOf(userTwoAddress)).to.eq(
|
||||
BigNumber.from(MAX_UINT256).sub(BigNumber.from(DEFAULT_COLLECT_PRICE).mul(2))
|
||||
);
|
||||
expect(await currency.balanceOf(userAddress)).to.eq(expectedRecipientAmount.mul(2));
|
||||
expect(await currency.balanceOf(treasuryAddress)).to.eq(expectedTreasuryAmount.mul(2));
|
||||
});
|
||||
|
||||
it('User should post with timed fee collect module as the collect module and data, user two mirrors, follows, then collects from their mirror and pays fee, fee distribution is valid', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_PRICE, currency.address, userAddress, REFERRAL_FEE_BPS]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: timedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
await expect(
|
||||
currency.connect(userTwo).approve(timedFeeCollectModule.address, MAX_UINT256)
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.not.be.reverted;
|
||||
|
||||
const expectedTreasuryAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.mul(TREASURY_FEE_BPS)
|
||||
.div(BPS_MAX);
|
||||
const expectedReferralAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.sub(expectedTreasuryAmount)
|
||||
.mul(REFERRAL_FEE_BPS)
|
||||
.div(BPS_MAX);
|
||||
const expectedReferrerAmount = BigNumber.from(MAX_UINT256)
|
||||
.sub(DEFAULT_COLLECT_PRICE)
|
||||
.add(expectedReferralAmount);
|
||||
const expectedRecipientAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.sub(expectedTreasuryAmount)
|
||||
.sub(expectedReferralAmount);
|
||||
|
||||
expect(await currency.balanceOf(userTwoAddress)).to.eq(expectedReferrerAmount);
|
||||
expect(await currency.balanceOf(userAddress)).to.eq(expectedRecipientAmount);
|
||||
expect(await currency.balanceOf(treasuryAddress)).to.eq(expectedTreasuryAmount);
|
||||
});
|
||||
|
||||
it('User should post with timed fee collect module as the collect module and data, with no referral fee, user two mirrors, follows, then collects from their mirror and pays fee, fee distribution is valid', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
const collectModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address', 'uint16'],
|
||||
[DEFAULT_COLLECT_PRICE, currency.address, userAddress, 0]
|
||||
);
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: timedFeeCollectModule.address,
|
||||
collectModuleData: collectModuleData,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
await expect(
|
||||
currency.connect(userTwo).approve(timedFeeCollectModule.address, MAX_UINT256)
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_COLLECT_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).collect(secondProfileId, 1, data)).to.not.be.reverted;
|
||||
|
||||
const expectedTreasuryAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
|
||||
.mul(TREASURY_FEE_BPS)
|
||||
.div(BPS_MAX);
|
||||
const expectedRecipientAmount =
|
||||
BigNumber.from(DEFAULT_COLLECT_PRICE).sub(expectedTreasuryAmount);
|
||||
|
||||
expect(await currency.balanceOf(userTwoAddress)).to.eq(
|
||||
BigNumber.from(MAX_UINT256).sub(DEFAULT_COLLECT_PRICE)
|
||||
);
|
||||
expect(await currency.balanceOf(userAddress)).to.eq(expectedRecipientAmount);
|
||||
expect(await currency.balanceOf(treasuryAddress)).to.eq(expectedTreasuryAmount);
|
||||
});
|
||||
});
|
||||
});
|
||||
238
test/modules/follow/approval-follow-module.spec.ts
Normal file
238
test/modules/follow/approval-follow-module.spec.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { ZERO_ADDRESS } from '../../helpers/constants';
|
||||
import { ERRORS } from '../../helpers/errors';
|
||||
import { getTimestamp, matchEvent, waitForTx } from '../../helpers/utils';
|
||||
import {
|
||||
abiCoder,
|
||||
approvalFollowModule,
|
||||
FIRST_PROFILE_ID,
|
||||
governance,
|
||||
lensHub,
|
||||
lensHubImpl,
|
||||
makeSuiteCleanRoom,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
user,
|
||||
userAddress,
|
||||
userTwo,
|
||||
userTwoAddress,
|
||||
} from '../../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Approval Follow Module', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistFollowModule(approvalFollowModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
context('Initialization', function () {
|
||||
it('Initialize call should fail when sender is not the hub', async function () {
|
||||
await expect(
|
||||
approvalFollowModule.initializeFollowModule(FIRST_PROFILE_ID, [])
|
||||
).to.be.revertedWith(ERRORS.NOT_HUB);
|
||||
});
|
||||
});
|
||||
|
||||
context('Approvals', function () {
|
||||
it('Approve should fail when calling it with addresses and toApprove params having different lengths', async function () {
|
||||
await expect(
|
||||
lensHub.setFollowModule(FIRST_PROFILE_ID, approvalFollowModule.address, [])
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
approvalFollowModule.connect(user).approve(FIRST_PROFILE_ID, [userTwoAddress], [])
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('Approve should fail when sender differs from profile owner', async function () {
|
||||
await expect(
|
||||
lensHub.setFollowModule(FIRST_PROFILE_ID, approvalFollowModule.address, [])
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
approvalFollowModule.connect(userTwo).approve(FIRST_PROFILE_ID, [userTwoAddress], [false])
|
||||
).to.be.revertedWith(ERRORS.NOT_PROFILE_OWNER);
|
||||
});
|
||||
});
|
||||
|
||||
context('Processing follow', function () {
|
||||
it('Process follow call should fail when sender is not the hub', async function () {
|
||||
await expect(
|
||||
approvalFollowModule.processFollow(userTwoAddress, FIRST_PROFILE_ID, [])
|
||||
).to.be.revertedWith(ERRORS.NOT_HUB);
|
||||
});
|
||||
|
||||
it('Follow should fail when follower address is not approved', async function () {
|
||||
await expect(
|
||||
lensHub.setFollowModule(FIRST_PROFILE_ID, approvalFollowModule.address, [])
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.be.revertedWith(
|
||||
ERRORS.FOLLOW_NOT_APPROVED
|
||||
);
|
||||
});
|
||||
|
||||
it('Follow should fail when follower address approval is revoked after being approved', async function () {
|
||||
const data = abiCoder.encode(['address[]'], [[userTwoAddress]]);
|
||||
await expect(
|
||||
lensHub.setFollowModule(FIRST_PROFILE_ID, approvalFollowModule.address, data)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
approvalFollowModule.connect(user).approve(FIRST_PROFILE_ID, [userTwoAddress], [false])
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.be.revertedWith(
|
||||
ERRORS.FOLLOW_NOT_APPROVED
|
||||
);
|
||||
});
|
||||
|
||||
it('Follow should fail when follower address is not approved even when following itself', async function () {
|
||||
await expect(
|
||||
lensHub.setFollowModule(FIRST_PROFILE_ID, approvalFollowModule.address, [])
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.be.revertedWith(
|
||||
ERRORS.FOLLOW_NOT_APPROVED
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
context('Initialization', function () {
|
||||
it('Profile creation with initial approval data should emit expected event', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
const data = abiCoder.encode(['address[]'], [[userTwoAddress]]);
|
||||
|
||||
const tx = lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: 'secondhandle',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: approvalFollowModule.address,
|
||||
followModuleData: data,
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
});
|
||||
|
||||
const receipt = await waitForTx(tx);
|
||||
|
||||
expect(receipt.logs.length).to.eq(2);
|
||||
matchEvent(receipt, 'Transfer', [ZERO_ADDRESS, userAddress, secondProfileId], lensHubImpl);
|
||||
matchEvent(receipt, 'ProfileCreated', [
|
||||
secondProfileId,
|
||||
userAddress,
|
||||
userAddress,
|
||||
'secondhandle',
|
||||
MOCK_PROFILE_URI,
|
||||
approvalFollowModule.address,
|
||||
data,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Setting follow module with initial approval data should emit expected event', async function () {
|
||||
const data = abiCoder.encode(['address[]'], [[userTwoAddress]]);
|
||||
const tx = lensHub.setFollowModule(FIRST_PROFILE_ID, approvalFollowModule.address, data);
|
||||
|
||||
const receipt = await waitForTx(tx);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'FollowModuleSet', [
|
||||
FIRST_PROFILE_ID,
|
||||
approvalFollowModule.address,
|
||||
data,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Setting follow module should work when calling it without initial approval data', async function () {
|
||||
await expect(
|
||||
lensHub.setFollowModule(FIRST_PROFILE_ID, approvalFollowModule.address, [])
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('Setting follow module should work when calling it with initial approval data', async function () {
|
||||
const data = abiCoder.encode(['address[]'], [[userTwoAddress]]);
|
||||
await expect(
|
||||
lensHub.setFollowModule(FIRST_PROFILE_ID, approvalFollowModule.address, data)
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
});
|
||||
});
|
||||
|
||||
context('Approvals and follows', function () {
|
||||
it('Approval should emit expected event', async function () {
|
||||
const tx = approvalFollowModule
|
||||
.connect(user)
|
||||
.approve(FIRST_PROFILE_ID, [userTwoAddress], [true]);
|
||||
|
||||
const receipt = await waitForTx(tx);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'FollowsApproved', [
|
||||
userAddress,
|
||||
FIRST_PROFILE_ID,
|
||||
[userTwoAddress],
|
||||
[true],
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Follow call should work when address was previously approved', async function () {
|
||||
await expect(
|
||||
lensHub.setFollowModule(FIRST_PROFILE_ID, approvalFollowModule.address, [])
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
approvalFollowModule.connect(user).approve(FIRST_PROFILE_ID, [userTwoAddress], [true])
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('Follow call to self should work when address was previously approved', async function () {
|
||||
await expect(
|
||||
lensHub.setFollowModule(FIRST_PROFILE_ID, approvalFollowModule.address, [])
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
approvalFollowModule.connect(user).approve(FIRST_PROFILE_ID, [userAddress], [true])
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
});
|
||||
});
|
||||
|
||||
context('View Functions', function () {
|
||||
beforeEach(async function () {
|
||||
const data = abiCoder.encode(['address[]'], [[userTwoAddress]]);
|
||||
await expect(
|
||||
lensHub.setFollowModule(FIRST_PROFILE_ID, approvalFollowModule.address, data)
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('Single approval getter should return expected values', async function () {
|
||||
expect(
|
||||
await approvalFollowModule.isApproved(userAddress, FIRST_PROFILE_ID, userTwoAddress)
|
||||
).to.eq(true);
|
||||
|
||||
expect(
|
||||
await approvalFollowModule.isApproved(userAddress, FIRST_PROFILE_ID, userAddress)
|
||||
).to.eq(false);
|
||||
});
|
||||
|
||||
it('Array approval getter should return expected values', async function () {
|
||||
const result = await approvalFollowModule.isApprovedArray(userAddress, FIRST_PROFILE_ID, [
|
||||
userTwoAddress,
|
||||
userAddress,
|
||||
]);
|
||||
expect(result[0]).to.eq(true);
|
||||
expect(result[1]).to.eq(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
313
test/modules/follow/fee-follow-module.spec.ts
Normal file
313
test/modules/follow/fee-follow-module.spec.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
import { BigNumber } from '@ethersproject/contracts/node_modules/@ethersproject/bignumber';
|
||||
import { parseEther } from '@ethersproject/units';
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { MAX_UINT256, ZERO_ADDRESS } from '../../helpers/constants';
|
||||
import { ERRORS } from '../../helpers/errors';
|
||||
import { getTimestamp, matchEvent, waitForTx } from '../../helpers/utils';
|
||||
import {
|
||||
abiCoder,
|
||||
BPS_MAX,
|
||||
currency,
|
||||
feeFollowModule,
|
||||
FIRST_PROFILE_ID,
|
||||
governance,
|
||||
lensHub,
|
||||
lensHubImpl,
|
||||
makeSuiteCleanRoom,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
moduleGlobals,
|
||||
treasuryAddress,
|
||||
TREASURY_FEE_BPS,
|
||||
userAddress,
|
||||
userTwo,
|
||||
userTwoAddress,
|
||||
} from '../../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Fee Follow Module', function () {
|
||||
const DEFAULT_FOLLOW_PRICE = parseEther('10');
|
||||
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistFollowModule(feeFollowModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
moduleGlobals.connect(governance).whitelistCurrency(currency.address, true)
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
context('Initialization', function () {
|
||||
it('user should fail to create a profile with fee follow module using unwhitelisted currency', async function () {
|
||||
const followModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address'],
|
||||
[DEFAULT_FOLLOW_PRICE, userTwoAddress, userAddress]
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: feeFollowModule.address,
|
||||
followModuleData: followModuleData,
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('user should fail to create a profile with fee follow module using zero recipient', async function () {
|
||||
const followModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address'],
|
||||
[DEFAULT_FOLLOW_PRICE, currency.address, ZERO_ADDRESS]
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: feeFollowModule.address,
|
||||
followModuleData: followModuleData,
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('user should fail to create a profile with fee follow module using amount lower than max BPS', async function () {
|
||||
const followModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address'],
|
||||
[9999, currency.address, userAddress]
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: feeFollowModule.address,
|
||||
followModuleData: followModuleData,
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
context('Following', function () {
|
||||
beforeEach(async function () {
|
||||
const followModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address'],
|
||||
[DEFAULT_FOLLOW_PRICE, currency.address, userAddress]
|
||||
);
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: feeFollowModule.address,
|
||||
followModuleData: followModuleData,
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('UserTwo should fail to follow passing a different expected price in data', async function () {
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_FOLLOW_PRICE.div(2)]
|
||||
);
|
||||
await expect(
|
||||
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [data])
|
||||
).to.be.revertedWith(ERRORS.MODULE_DATA_MISMATCH);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to follow passing a different expected currency in data', async function () {
|
||||
const data = abiCoder.encode(['address', 'uint256'], [userAddress, DEFAULT_FOLLOW_PRICE]);
|
||||
await expect(
|
||||
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [data])
|
||||
).to.be.revertedWith(ERRORS.MODULE_DATA_MISMATCH);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to follow without first approving module with currency', async function () {
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_FOLLOW_PRICE]
|
||||
);
|
||||
await expect(
|
||||
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [data])
|
||||
).to.be.revertedWith(ERRORS.ERC20_TRANSFER_EXCEEDS_ALLOWANCE);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('User should create a profile with the fee follow module as the follow module and data, correct events should be emitted', async function () {
|
||||
const followModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address'],
|
||||
[DEFAULT_FOLLOW_PRICE, currency.address, userAddress]
|
||||
);
|
||||
const tx = lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: feeFollowModule.address,
|
||||
followModuleData: followModuleData,
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
});
|
||||
|
||||
const receipt = await waitForTx(tx);
|
||||
|
||||
expect(receipt.logs.length).to.eq(2);
|
||||
matchEvent(receipt, 'Transfer', [ZERO_ADDRESS, userAddress, FIRST_PROFILE_ID], lensHubImpl);
|
||||
matchEvent(receipt, 'ProfileCreated', [
|
||||
FIRST_PROFILE_ID,
|
||||
userAddress,
|
||||
userAddress,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
feeFollowModule.address,
|
||||
followModuleData,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('User should create a profile then set the fee follow module as the follow module with data, correct events should be emitted', async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const followModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address'],
|
||||
[DEFAULT_FOLLOW_PRICE, currency.address, userAddress]
|
||||
);
|
||||
const tx = lensHub.setFollowModule(
|
||||
FIRST_PROFILE_ID,
|
||||
feeFollowModule.address,
|
||||
followModuleData
|
||||
);
|
||||
|
||||
const receipt = await waitForTx(tx);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'FollowModuleSet', [
|
||||
FIRST_PROFILE_ID,
|
||||
feeFollowModule.address,
|
||||
followModuleData,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('User should create a profile with the fee follow module as the follow module and data, fetched profile data should be accurate', async function () {
|
||||
const followModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address'],
|
||||
[DEFAULT_FOLLOW_PRICE, currency.address, userAddress]
|
||||
);
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: feeFollowModule.address,
|
||||
followModuleData: followModuleData,
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const fetchedData = await feeFollowModule.getProfileData(FIRST_PROFILE_ID);
|
||||
expect(fetchedData.amount).to.eq(DEFAULT_FOLLOW_PRICE);
|
||||
expect(fetchedData.recipient).to.eq(userAddress);
|
||||
expect(fetchedData.currency).to.eq(currency.address);
|
||||
});
|
||||
|
||||
it('User should create a profile with the fee follow module as the follow module and data, user two follows, fee distribution is valid', async function () {
|
||||
const followModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address'],
|
||||
[DEFAULT_FOLLOW_PRICE, currency.address, userAddress]
|
||||
);
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: feeFollowModule.address,
|
||||
followModuleData: followModuleData,
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
await expect(
|
||||
currency.connect(userTwo).approve(feeFollowModule.address, MAX_UINT256)
|
||||
).to.not.be.reverted;
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_FOLLOW_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [data])).to.not.be.reverted;
|
||||
|
||||
const expectedTreasuryAmount = BigNumber.from(DEFAULT_FOLLOW_PRICE)
|
||||
.mul(TREASURY_FEE_BPS)
|
||||
.div(BPS_MAX);
|
||||
const expectedRecipientAmount =
|
||||
BigNumber.from(DEFAULT_FOLLOW_PRICE).sub(expectedTreasuryAmount);
|
||||
|
||||
expect(await currency.balanceOf(userTwoAddress)).to.eq(
|
||||
BigNumber.from(MAX_UINT256).sub(DEFAULT_FOLLOW_PRICE)
|
||||
);
|
||||
expect(await currency.balanceOf(userAddress)).to.eq(expectedRecipientAmount);
|
||||
expect(await currency.balanceOf(treasuryAddress)).to.eq(expectedTreasuryAmount);
|
||||
});
|
||||
|
||||
it('User should create a profile with the fee follow module as the follow module and data, user two follows twice, fee distribution is valid', async function () {
|
||||
const followModuleData = abiCoder.encode(
|
||||
['uint256', 'address', 'address'],
|
||||
[DEFAULT_FOLLOW_PRICE, currency.address, userAddress]
|
||||
);
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: feeFollowModule.address,
|
||||
followModuleData: followModuleData,
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
|
||||
await expect(
|
||||
currency.connect(userTwo).approve(feeFollowModule.address, MAX_UINT256)
|
||||
).to.not.be.reverted;
|
||||
const data = abiCoder.encode(
|
||||
['address', 'uint256'],
|
||||
[currency.address, DEFAULT_FOLLOW_PRICE]
|
||||
);
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [data])).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [data])).to.not.be.reverted;
|
||||
|
||||
const expectedTreasuryAmount = BigNumber.from(DEFAULT_FOLLOW_PRICE)
|
||||
.mul(TREASURY_FEE_BPS)
|
||||
.div(BPS_MAX);
|
||||
const expectedRecipientAmount =
|
||||
BigNumber.from(DEFAULT_FOLLOW_PRICE).sub(expectedTreasuryAmount);
|
||||
|
||||
expect(await currency.balanceOf(userTwoAddress)).to.eq(
|
||||
BigNumber.from(MAX_UINT256).sub(BigNumber.from(DEFAULT_FOLLOW_PRICE).mul(2))
|
||||
);
|
||||
expect(await currency.balanceOf(userAddress)).to.eq(expectedRecipientAmount.mul(2));
|
||||
expect(await currency.balanceOf(treasuryAddress)).to.eq(expectedTreasuryAmount.mul(2));
|
||||
});
|
||||
});
|
||||
});
|
||||
254
test/modules/reference/follower-only-reference-module.spec.ts
Normal file
254
test/modules/reference/follower-only-reference-module.spec.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { FollowNFT__factory } from '../../../typechain-types';
|
||||
import { ZERO_ADDRESS } from '../../helpers/constants';
|
||||
import { ERRORS } from '../../helpers/errors';
|
||||
import { getTimestamp, matchEvent, waitForTx } from '../../helpers/utils';
|
||||
import {
|
||||
emptyCollectModule,
|
||||
FIRST_PROFILE_ID,
|
||||
followerOnlyReferenceModule,
|
||||
governance,
|
||||
lensHub,
|
||||
makeSuiteCleanRoom,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
MOCK_URI,
|
||||
user,
|
||||
userAddress,
|
||||
userTwo,
|
||||
userTwoAddress,
|
||||
} from '../../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Follower Only Reference Module', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub
|
||||
.connect(governance)
|
||||
.whitelistReferenceModule(followerOnlyReferenceModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: followerOnlyReferenceModule.address,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
// We don't need a `publishing` or `initialization` context because initialization never reverts in the FollowerOnlyReferenceModule.
|
||||
context('Commenting', function () {
|
||||
it('Commenting should fail if commenter is not a follower and follow NFT not yet deployed', async function () {
|
||||
await expect(
|
||||
lensHub.comment({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
|
||||
});
|
||||
|
||||
it('Commenting should fail if commenter follows, then transfers the follow NFT before attempting to comment', async function () {
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const followNFT = FollowNFT__factory.connect(
|
||||
await lensHub.getFollowNFT(FIRST_PROFILE_ID),
|
||||
user
|
||||
);
|
||||
|
||||
await expect(followNFT.transferFrom(userAddress, userTwoAddress, 1)).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.comment({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
context('Mirroring', function () {
|
||||
it('Mirroring should fail if publisher is not a follower and follow NFT not yet deployed', async function () {
|
||||
await expect(
|
||||
lensHub.mirror({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
|
||||
});
|
||||
|
||||
it('Mirroring should fail if publisher follows, then transfers the follow NFT before attempting to mirror', async function () {
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const followNFT = FollowNFT__factory.connect(
|
||||
await lensHub.getFollowNFT(FIRST_PROFILE_ID),
|
||||
user
|
||||
);
|
||||
|
||||
await expect(followNFT.transferFrom(userAddress, userTwoAddress, 1)).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.mirror({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
context('Publishing', function () {
|
||||
it('Posting with follower only reference module as reference module should emit expected events', async function () {
|
||||
const tx = lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: followerOnlyReferenceModule.address,
|
||||
referenceModuleData: [],
|
||||
});
|
||||
const receipt = await waitForTx(tx);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'PostCreated', [
|
||||
FIRST_PROFILE_ID,
|
||||
2,
|
||||
MOCK_URI,
|
||||
emptyCollectModule.address,
|
||||
[],
|
||||
followerOnlyReferenceModule.address,
|
||||
[],
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
context('Commenting', function () {
|
||||
it('Commenting should work if the commenter is a follower', async function () {
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const followNFT = FollowNFT__factory.connect(
|
||||
await lensHub.getFollowNFT(FIRST_PROFILE_ID),
|
||||
user
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.comment({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('Commenting should work if the commenter follows, transfers the follow NFT then receives it back before attempting to comment', async function () {
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const followNFT = FollowNFT__factory.connect(
|
||||
await lensHub.getFollowNFT(FIRST_PROFILE_ID),
|
||||
user
|
||||
);
|
||||
|
||||
await expect(followNFT.transferFrom(userAddress, userTwoAddress, 1)).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
followNFT.connect(userTwo).transferFrom(userTwoAddress, userAddress, 1)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.comment({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
});
|
||||
|
||||
context('Mirroring', function () {
|
||||
it('Mirroring should work if publisher is a follower', async function () {
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const followNFT = FollowNFT__factory.connect(
|
||||
await lensHub.getFollowNFT(FIRST_PROFILE_ID),
|
||||
user
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.mirror({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('Mirroring should work if publisher follows, transfers the follow NFT then receives it back before attempting to mirror', async function () {
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const followNFT = FollowNFT__factory.connect(
|
||||
await lensHub.getFollowNFT(FIRST_PROFILE_ID),
|
||||
user
|
||||
);
|
||||
|
||||
await expect(followNFT.transferFrom(userAddress, userTwoAddress, 1)).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
followNFT.connect(userTwo).transferFrom(userTwoAddress, userAddress, 1)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.mirror({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
92
test/nft/collect-nft.spec.ts
Normal file
92
test/nft/collect-nft.spec.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { CollectNFT, CollectNFT__factory } from '../../typechain-types';
|
||||
import { ZERO_ADDRESS } from '../helpers/constants';
|
||||
import { ERRORS } from '../helpers/errors';
|
||||
import {
|
||||
emptyCollectModule,
|
||||
FIRST_PROFILE_ID,
|
||||
governance,
|
||||
lensHub,
|
||||
makeSuiteCleanRoom,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
MOCK_URI,
|
||||
user,
|
||||
userAddress,
|
||||
userTwo,
|
||||
} from '../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Collect NFT', function () {
|
||||
let collectNFT: CollectNFT;
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
await expect(lensHub.collect(FIRST_PROFILE_ID, 1, [])).to.not.be.reverted;
|
||||
collectNFT = CollectNFT__factory.connect(
|
||||
await lensHub.getCollectNFT(FIRST_PROFILE_ID, 1),
|
||||
user
|
||||
);
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
it('User should fail to reinitialize the collect NFT', async function () {
|
||||
await expect(collectNFT.initialize(FIRST_PROFILE_ID, 1, 'name', 'symbol')).to.be.revertedWith(
|
||||
ERRORS.INITIALIZED
|
||||
);
|
||||
});
|
||||
|
||||
it('User should fail to mint on the collect NFT', async function () {
|
||||
await expect(collectNFT.mint(userAddress)).to.be.revertedWith(ERRORS.NOT_HUB);
|
||||
});
|
||||
|
||||
it("UserTwo should fail to burn user's collect NFT", async function () {
|
||||
await expect(collectNFT.connect(userTwo).burn(1)).to.be.revertedWith(
|
||||
ERRORS.NOT_OWNER_OR_APPROVED
|
||||
);
|
||||
});
|
||||
|
||||
it('User should fail to get the URI for a token that does not exist', async function () {
|
||||
await expect(collectNFT.tokenURI(2)).to.be.revertedWith(ERRORS.TOKEN_DOES_NOT_EXIST);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('Collect NFT URI should be valid', async function () {
|
||||
expect(await collectNFT.tokenURI(1)).to.eq(MOCK_URI);
|
||||
});
|
||||
|
||||
it('Collect NFT source publication pointer should be accurate', async function () {
|
||||
const pointer = await collectNFT.getSourcePublicationPointer();
|
||||
expect(pointer[0]).to.eq(FIRST_PROFILE_ID);
|
||||
expect(pointer[1]).to.eq(1);
|
||||
});
|
||||
|
||||
it('User should burn their collect NFT', async function () {
|
||||
await expect(collectNFT.burn(1)).to.not.be.reverted;
|
||||
});
|
||||
});
|
||||
});
|
||||
427
test/nft/follow-nft.spec.ts
Normal file
427
test/nft/follow-nft.spec.ts
Normal file
@@ -0,0 +1,427 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { FollowNFT, FollowNFT__factory } from '../../typechain-types';
|
||||
import { MAX_UINT256, ZERO_ADDRESS } from '../helpers/constants';
|
||||
import { ERRORS } from '../helpers/errors';
|
||||
import {
|
||||
cancelWithPermitForAll,
|
||||
getBlockNumber,
|
||||
getDelegateBySigParts,
|
||||
mine,
|
||||
} from '../helpers/utils';
|
||||
import {
|
||||
FIRST_PROFILE_ID,
|
||||
governanceAddress,
|
||||
helper,
|
||||
lensHub,
|
||||
makeSuiteCleanRoom,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
OTHER_MOCK_URI,
|
||||
testWallet,
|
||||
user,
|
||||
userAddress,
|
||||
userTwo,
|
||||
userTwoAddress,
|
||||
} from '../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Follow NFT', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('generic', function () {
|
||||
context('Negatives', function () {
|
||||
it('User should follow, and fail to re-initialize the follow NFT', async function () {
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const followNFT = FollowNFT__factory.connect(
|
||||
await lensHub.getFollowNFT(FIRST_PROFILE_ID),
|
||||
user
|
||||
);
|
||||
|
||||
await expect(followNFT.initialize(FIRST_PROFILE_ID, 'NAME', 'SYMBOL')).to.be.revertedWith(
|
||||
ERRORS.INITIALIZED
|
||||
);
|
||||
});
|
||||
|
||||
it("User should follow, userTwo should fail to burn user's follow NFT", async function () {
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const followNFT = FollowNFT__factory.connect(
|
||||
await lensHub.getFollowNFT(FIRST_PROFILE_ID),
|
||||
userTwo
|
||||
);
|
||||
|
||||
await expect(followNFT.burn(1)).to.be.revertedWith(ERRORS.NOT_OWNER_OR_APPROVED);
|
||||
});
|
||||
|
||||
it('User should follow, then fail to mint a follow NFT directly', async function () {
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const followNFT = FollowNFT__factory.connect(
|
||||
await lensHub.getFollowNFT(FIRST_PROFILE_ID),
|
||||
user
|
||||
);
|
||||
|
||||
await expect(followNFT.mint(userAddress)).to.be.revertedWith(ERRORS.NOT_HUB);
|
||||
});
|
||||
|
||||
it('User should follow, then fail to get the power at a future block', async function () {
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const followNFT = FollowNFT__factory.connect(
|
||||
await lensHub.getFollowNFT(FIRST_PROFILE_ID),
|
||||
user
|
||||
);
|
||||
|
||||
const blockNumber = await getBlockNumber();
|
||||
|
||||
await expect(
|
||||
followNFT.getPowerByBlockNumber(userAddress, blockNumber + 1)
|
||||
).to.be.revertedWith(ERRORS.BLOCK_NUMBER_INVALID);
|
||||
});
|
||||
|
||||
it('user should follow, then fail to get the URI for a token that does not exist', async function () {
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const followNFT = FollowNFT__factory.connect(
|
||||
await lensHub.getFollowNFT(FIRST_PROFILE_ID),
|
||||
user
|
||||
);
|
||||
await expect(followNFT.tokenURI(2)).to.be.revertedWith(ERRORS.TOKEN_DOES_NOT_EXIST);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('User should follow, then burn their follow NFT', async function () {
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const followNFT = FollowNFT__factory.connect(
|
||||
await lensHub.getFollowNFT(FIRST_PROFILE_ID),
|
||||
user
|
||||
);
|
||||
|
||||
await expect(followNFT.burn(1)).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('User should follow, delegate to themself, governance power should be zero before the last block, and 1 at the current block', async function () {
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const followNFT = FollowNFT__factory.connect(
|
||||
await lensHub.getFollowNFT(FIRST_PROFILE_ID),
|
||||
user
|
||||
);
|
||||
|
||||
await expect(followNFT.delegate(userAddress)).to.not.be.reverted;
|
||||
|
||||
const blockNumber = await getBlockNumber();
|
||||
|
||||
expect(await followNFT.getPowerByBlockNumber(userAddress, blockNumber - 1)).to.eq(0);
|
||||
expect(await followNFT.getPowerByBlockNumber(userAddress, blockNumber)).to.eq(1);
|
||||
});
|
||||
|
||||
it('User and userTwo should follow, governance power should be zero, then users delegate multiple times, governance power should be accurate throughout', async function () {
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const followNFT = FollowNFT__factory.connect(
|
||||
await lensHub.getFollowNFT(FIRST_PROFILE_ID),
|
||||
user
|
||||
);
|
||||
|
||||
const firstCheckpointBlock = await getBlockNumber();
|
||||
|
||||
await expect(followNFT.delegate(userAddress)).to.not.be.reverted;
|
||||
await expect(followNFT.connect(userTwo).delegate(userTwoAddress)).to.not.be.reverted;
|
||||
const secondCheckpointBlock = await getBlockNumber();
|
||||
|
||||
await expect(followNFT.connect(userTwo).delegate(userAddress)).to.not.be.reverted;
|
||||
const thirdCheckpointBlock = await getBlockNumber();
|
||||
|
||||
await expect(followNFT.delegate(userTwoAddress)).to.not.be.reverted;
|
||||
const fourthCheckpointBlock = await getBlockNumber();
|
||||
|
||||
await expect(followNFT.delegate(governanceAddress)).to.not.be.reverted;
|
||||
await expect(followNFT.connect(userTwo).delegate(governanceAddress)).to.not.be.reverted;
|
||||
const fifthCheckpointBlock = await getBlockNumber();
|
||||
|
||||
await expect(followNFT.delegate(ZERO_ADDRESS)).to.not.be.reverted;
|
||||
await expect(followNFT.connect(userTwo).delegate(ZERO_ADDRESS)).to.not.be.reverted;
|
||||
const sixthCheckpointBlock = await getBlockNumber();
|
||||
|
||||
await expect(followNFT.delegate(userAddress)).to.not.be.reverted;
|
||||
await expect(followNFT.connect(userTwo).delegate(userAddress)).to.not.be.reverted;
|
||||
const seventhCheckpointBlock = await getBlockNumber();
|
||||
|
||||
// First validation
|
||||
expect(await followNFT.getPowerByBlockNumber(userAddress, firstCheckpointBlock)).to.eq(0);
|
||||
expect(await followNFT.getPowerByBlockNumber(userTwoAddress, firstCheckpointBlock)).to.eq(
|
||||
0
|
||||
);
|
||||
|
||||
// Second validation
|
||||
expect(await followNFT.getPowerByBlockNumber(userAddress, secondCheckpointBlock)).to.eq(1);
|
||||
expect(await followNFT.getPowerByBlockNumber(userTwoAddress, secondCheckpointBlock)).to.eq(
|
||||
1
|
||||
);
|
||||
|
||||
// Third validation
|
||||
expect(await followNFT.getPowerByBlockNumber(userAddress, thirdCheckpointBlock)).to.eq(2);
|
||||
expect(await followNFT.getPowerByBlockNumber(userTwoAddress, thirdCheckpointBlock)).to.eq(
|
||||
0
|
||||
);
|
||||
|
||||
// Fourth validation
|
||||
expect(await followNFT.getPowerByBlockNumber(userAddress, fourthCheckpointBlock)).to.eq(1);
|
||||
expect(await followNFT.getPowerByBlockNumber(userTwoAddress, fourthCheckpointBlock)).to.eq(
|
||||
1
|
||||
);
|
||||
|
||||
// Fifth validation
|
||||
expect(await followNFT.getPowerByBlockNumber(userAddress, fifthCheckpointBlock)).to.eq(0);
|
||||
expect(await followNFT.getPowerByBlockNumber(userTwoAddress, fifthCheckpointBlock)).to.eq(
|
||||
0
|
||||
);
|
||||
expect(
|
||||
await followNFT.getPowerByBlockNumber(governanceAddress, fifthCheckpointBlock)
|
||||
).to.eq(2);
|
||||
|
||||
// Sixth validation
|
||||
expect(await followNFT.getPowerByBlockNumber(userAddress, sixthCheckpointBlock)).to.eq(0);
|
||||
expect(await followNFT.getPowerByBlockNumber(userTwoAddress, sixthCheckpointBlock)).to.eq(
|
||||
0
|
||||
);
|
||||
expect(
|
||||
await followNFT.getPowerByBlockNumber(governanceAddress, sixthCheckpointBlock)
|
||||
).to.eq(0);
|
||||
expect(await followNFT.getPowerByBlockNumber(ZERO_ADDRESS, sixthCheckpointBlock)).to.eq(0);
|
||||
|
||||
// Seventh validation
|
||||
expect(await followNFT.getPowerByBlockNumber(userAddress, seventhCheckpointBlock)).to.eq(2);
|
||||
expect(await followNFT.getPowerByBlockNumber(userTwoAddress, seventhCheckpointBlock)).to.eq(
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it('User and userTwo should follow, delegate to themselves, 10 blocks later user delegates to userTwo, 10 blocks later both delegate to user, governance power should be accurate throughout', async function () {
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const followNFT = FollowNFT__factory.connect(
|
||||
await lensHub.getFollowNFT(FIRST_PROFILE_ID),
|
||||
user
|
||||
);
|
||||
|
||||
await expect(followNFT.delegate(userAddress)).to.not.be.reverted;
|
||||
await expect(followNFT.connect(userTwo).delegate(userTwoAddress)).to.not.be.reverted;
|
||||
const firstCheckpointBlock = await getBlockNumber();
|
||||
|
||||
await mine(10);
|
||||
|
||||
await expect(followNFT.delegate(userTwoAddress)).to.not.be.reverted;
|
||||
const secondCheckpointBlock = await getBlockNumber();
|
||||
|
||||
await mine(10);
|
||||
|
||||
await expect(followNFT.delegate(userAddress)).to.not.be.reverted;
|
||||
await expect(followNFT.connect(userTwo).delegate(userAddress)).to.not.be.reverted;
|
||||
const thirdCheckpointBlock = await getBlockNumber();
|
||||
|
||||
// First validation
|
||||
expect(await followNFT.getPowerByBlockNumber(userAddress, firstCheckpointBlock)).to.eq(1);
|
||||
expect(await followNFT.getPowerByBlockNumber(userTwoAddress, firstCheckpointBlock)).to.eq(
|
||||
1
|
||||
);
|
||||
|
||||
// Second validation
|
||||
expect(await followNFT.getPowerByBlockNumber(userAddress, secondCheckpointBlock)).to.eq(0);
|
||||
expect(await followNFT.getPowerByBlockNumber(userTwoAddress, secondCheckpointBlock)).to.eq(
|
||||
2
|
||||
);
|
||||
|
||||
// Last validation
|
||||
expect(await followNFT.getPowerByBlockNumber(userAddress, thirdCheckpointBlock)).to.eq(2);
|
||||
expect(await followNFT.getPowerByBlockNumber(userTwoAddress, thirdCheckpointBlock)).to.eq(
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it('user and userTwo should follow, user delegates to userTwo twice, governance power should be accurate', async function () {
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const followNFT = FollowNFT__factory.connect(
|
||||
await lensHub.getFollowNFT(FIRST_PROFILE_ID),
|
||||
user
|
||||
);
|
||||
|
||||
await expect(followNFT.delegate(userTwoAddress)).to.not.be.reverted;
|
||||
await expect(followNFT.delegate(userTwoAddress)).to.not.be.reverted;
|
||||
|
||||
const blockNumber = await getBlockNumber();
|
||||
expect(await followNFT.getPowerByBlockNumber(userAddress, blockNumber)).to.eq(0);
|
||||
expect(await followNFT.getPowerByBlockNumber(userTwoAddress, blockNumber)).to.eq(1);
|
||||
});
|
||||
|
||||
it('User and userTwo should follow, then transfer their NFTs to the helper contract, then the helper contract batch delegates to user one, then user two, governance power should be accurate', async function () {
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const followNFT = FollowNFT__factory.connect(
|
||||
await lensHub.getFollowNFT(FIRST_PROFILE_ID),
|
||||
user
|
||||
);
|
||||
|
||||
await expect(followNFT.transferFrom(userAddress, helper.address, 1)).to.not.be.reverted;
|
||||
await expect(
|
||||
followNFT.connect(userTwo).transferFrom(userTwoAddress, helper.address, 2)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
helper.batchDelegate(followNFT.address, userAddress, userTwoAddress)
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('user should follow, then get the URI for their token, URI should be accurate', async function () {
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
const followNFT = FollowNFT__factory.connect(
|
||||
await lensHub.getFollowNFT(FIRST_PROFILE_ID),
|
||||
user
|
||||
);
|
||||
expect(await followNFT.tokenURI(1)).to.eq(MOCK_FOLLOW_NFT_URI);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('meta-tx', function () {
|
||||
let followNFT: FollowNFT;
|
||||
beforeEach(async function () {
|
||||
await expect(lensHub.connect(testWallet).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
followNFT = FollowNFT__factory.connect(await lensHub.getFollowNFT(FIRST_PROFILE_ID), user);
|
||||
});
|
||||
|
||||
context('negatives', function () {
|
||||
it('TestWallet should fail to delegate with sig with signature deadline mismatch', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getDelegateBySigParts(
|
||||
followNFT.address,
|
||||
await followNFT.name(),
|
||||
testWallet.address,
|
||||
userAddress,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
|
||||
await expect(
|
||||
followNFT.delegateBySig(testWallet.address, userAddress, {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to delegate with sig with invalid deadline', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getDelegateBySigParts(
|
||||
followNFT.address,
|
||||
await followNFT.name(),
|
||||
testWallet.address,
|
||||
userAddress,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
|
||||
await expect(
|
||||
followNFT.delegateBySig(testWallet.address, userAddress, {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: '0',
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_EXPIRED);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to delegate with sig with invalid nonce', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getDelegateBySigParts(
|
||||
followNFT.address,
|
||||
await followNFT.name(),
|
||||
testWallet.address,
|
||||
userAddress,
|
||||
nonce + 1,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
followNFT.delegateBySig(testWallet.address, userAddress, {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should sign attempt to delegate by sig, cancel with empty permitForAll, then fail to delegate by sig', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getDelegateBySigParts(
|
||||
followNFT.address,
|
||||
await followNFT.name(),
|
||||
testWallet.address,
|
||||
userAddress,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await cancelWithPermitForAll(followNFT.address);
|
||||
|
||||
await expect(
|
||||
followNFT.delegateBySig(testWallet.address, userAddress, {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('TestWallet should delegate by sig to user, governance power should be accurate before and after', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getDelegateBySigParts(
|
||||
followNFT.address,
|
||||
await followNFT.name(),
|
||||
testWallet.address,
|
||||
userAddress,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
let blockNumber = await getBlockNumber();
|
||||
expect(await followNFT.getPowerByBlockNumber(userAddress, blockNumber)).to.eq(0);
|
||||
expect(await followNFT.getPowerByBlockNumber(testWallet.address, blockNumber)).to.eq(0);
|
||||
|
||||
await expect(
|
||||
followNFT.delegateBySig(testWallet.address, userAddress, {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
blockNumber = await getBlockNumber();
|
||||
expect(await followNFT.getPowerByBlockNumber(userAddress, blockNumber)).to.eq(1);
|
||||
expect(await followNFT.getPowerByBlockNumber(userTwoAddress, blockNumber)).to.eq(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
485
test/nft/lens-nft-base.spec.ts
Normal file
485
test/nft/lens-nft-base.spec.ts
Normal file
@@ -0,0 +1,485 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { keccak256, toUtf8Bytes } from 'ethers/lib/utils';
|
||||
import { MAX_UINT256, ZERO_ADDRESS } from '../helpers/constants';
|
||||
import { ERRORS } from '../helpers/errors';
|
||||
import {
|
||||
cancelWithPermitForAll,
|
||||
getBurnWithSigparts,
|
||||
getChainId,
|
||||
getPermitForAllParts,
|
||||
getPermitParts,
|
||||
} from '../helpers/utils';
|
||||
import {
|
||||
abiCoder,
|
||||
FIRST_PROFILE_ID,
|
||||
lensHub,
|
||||
LENS_HUB_NFT_NAME,
|
||||
makeSuiteCleanRoom,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
testWallet,
|
||||
user,
|
||||
userAddress,
|
||||
} from '../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Lens NFT Base Functionality', function () {
|
||||
context('generic', function () {
|
||||
it('Domain separator fetched from contract should be accurate', async function () {
|
||||
const expectedDomainSeparator = keccak256(
|
||||
abiCoder.encode(
|
||||
['bytes32', 'bytes32', 'bytes32', 'uint256', 'address'],
|
||||
[
|
||||
keccak256(
|
||||
toUtf8Bytes(
|
||||
'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'
|
||||
)
|
||||
),
|
||||
keccak256(toUtf8Bytes(LENS_HUB_NFT_NAME)),
|
||||
keccak256(toUtf8Bytes('1')),
|
||||
getChainId(),
|
||||
lensHub.address,
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
expect(await lensHub.getDomainSeparator()).to.eq(expectedDomainSeparator);
|
||||
});
|
||||
});
|
||||
|
||||
context('meta-tx', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.connect(testWallet).createProfile({
|
||||
to: testWallet.address,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
it('TestWallet should fail to permit with zero spender', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getPermitParts(
|
||||
lensHub.address,
|
||||
await lensHub.name(),
|
||||
ZERO_ADDRESS,
|
||||
FIRST_PROFILE_ID,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.permit(ZERO_ADDRESS, FIRST_PROFILE_ID, {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.ZERO_SPENDER);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to permit with invalid token ID', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getPermitParts(
|
||||
lensHub.address,
|
||||
await lensHub.name(),
|
||||
userAddress,
|
||||
0,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.permit(userAddress, 0, {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.ERC721_QUERY_FOR_NONEXISTENT_TOKEN);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to permit with signature deadline mismatch', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getPermitParts(
|
||||
lensHub.address,
|
||||
await lensHub.name(),
|
||||
userAddress,
|
||||
FIRST_PROFILE_ID,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.permit(userAddress, FIRST_PROFILE_ID, {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to permit with invalid deadline', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getPermitParts(
|
||||
lensHub.address,
|
||||
await lensHub.name(),
|
||||
userAddress,
|
||||
FIRST_PROFILE_ID,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.permit(userAddress, FIRST_PROFILE_ID, {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: '0',
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_EXPIRED);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to permit with invalid nonce', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getPermitParts(
|
||||
lensHub.address,
|
||||
LENS_HUB_NFT_NAME,
|
||||
userAddress,
|
||||
FIRST_PROFILE_ID,
|
||||
nonce + 1,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.permit(userAddress, FIRST_PROFILE_ID, {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should sign attempt to permit, cancel with empty permitForAll, then fail to permit', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getPermitParts(
|
||||
lensHub.address,
|
||||
LENS_HUB_NFT_NAME,
|
||||
userAddress,
|
||||
FIRST_PROFILE_ID,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await cancelWithPermitForAll();
|
||||
|
||||
await expect(
|
||||
lensHub.permit(userAddress, FIRST_PROFILE_ID, {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to permitForAll with zero spender', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getPermitForAllParts(
|
||||
lensHub.address,
|
||||
LENS_HUB_NFT_NAME,
|
||||
testWallet.address,
|
||||
ZERO_ADDRESS,
|
||||
true,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.permitForAll(testWallet.address, ZERO_ADDRESS, true, {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.ZERO_SPENDER);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to permitForAll with signature deadline mismatch', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getPermitForAllParts(
|
||||
lensHub.address,
|
||||
LENS_HUB_NFT_NAME,
|
||||
testWallet.address,
|
||||
userAddress,
|
||||
true,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.permitForAll(testWallet.address, userAddress, true, {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to permitForAll with invalid deadline', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getPermitForAllParts(
|
||||
lensHub.address,
|
||||
LENS_HUB_NFT_NAME,
|
||||
testWallet.address,
|
||||
userAddress,
|
||||
true,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.permitForAll(testWallet.address, userAddress, true, {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: '0',
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_EXPIRED);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to permitForAll with invalid nonce', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getPermitForAllParts(
|
||||
lensHub.address,
|
||||
LENS_HUB_NFT_NAME,
|
||||
testWallet.address,
|
||||
userAddress,
|
||||
true,
|
||||
nonce + 1,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.permitForAll(testWallet.address, userAddress, true, {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should sign attempt to permitForAll, cancel with empty permitForAll, then fail to permitForAll', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getPermitForAllParts(
|
||||
lensHub.address,
|
||||
LENS_HUB_NFT_NAME,
|
||||
testWallet.address,
|
||||
userAddress,
|
||||
true,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await cancelWithPermitForAll();
|
||||
|
||||
await expect(
|
||||
lensHub.permitForAll(testWallet.address, userAddress, true, {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to burnWithSig with invalid token ID', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getBurnWithSigparts(
|
||||
lensHub.address,
|
||||
LENS_HUB_NFT_NAME,
|
||||
0,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(lensHub.burnWithSig(0, { v, r, s, deadline: MAX_UINT256 })).to.be.revertedWith(
|
||||
ERRORS.ERC721_QUERY_FOR_NONEXISTENT_TOKEN
|
||||
);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to burnWithSig with signature deadline mismatch', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getBurnWithSigparts(
|
||||
lensHub.address,
|
||||
LENS_HUB_NFT_NAME,
|
||||
FIRST_PROFILE_ID,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.burnWithSig(FIRST_PROFILE_ID, { v, r, s, deadline: MAX_UINT256 })
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to burnWithSig with invalid deadline', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getBurnWithSigparts(
|
||||
lensHub.address,
|
||||
LENS_HUB_NFT_NAME,
|
||||
FIRST_PROFILE_ID,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.burnWithSig(FIRST_PROFILE_ID, { v, r, s, deadline: '0' })
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_EXPIRED);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to burnWithSig with invalid nonce', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getBurnWithSigparts(
|
||||
lensHub.address,
|
||||
LENS_HUB_NFT_NAME,
|
||||
FIRST_PROFILE_ID,
|
||||
nonce + 1,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.burnWithSig(FIRST_PROFILE_ID, { v, r, s, deadline: MAX_UINT256 })
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should sign attempt to burnWithSig, cancel with empty permitForAll, then fail to burnWithSig', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getBurnWithSigparts(
|
||||
lensHub.address,
|
||||
LENS_HUB_NFT_NAME,
|
||||
FIRST_PROFILE_ID,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await cancelWithPermitForAll();
|
||||
|
||||
await expect(
|
||||
lensHub.burnWithSig(FIRST_PROFILE_ID, { v, r, s, deadline: MAX_UINT256 })
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('TestWallet should permit user, user should transfer NFT, send back NFT and fail to transfer it again', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getPermitParts(
|
||||
lensHub.address,
|
||||
LENS_HUB_NFT_NAME,
|
||||
userAddress,
|
||||
FIRST_PROFILE_ID,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.permit(userAddress, FIRST_PROFILE_ID, {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.transferFrom(testWallet.address, userAddress, FIRST_PROFILE_ID)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.transferFrom(userAddress, testWallet.address, FIRST_PROFILE_ID)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.transferFrom(testWallet.address, userAddress, FIRST_PROFILE_ID)
|
||||
).to.be.revertedWith(ERRORS.ERC721_TRANSFER_NOT_OWNER_OR_APPROVED);
|
||||
});
|
||||
|
||||
it('TestWallet should permitForAll user, user should transfer NFT, send back NFT and transfer it again', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getPermitForAllParts(
|
||||
lensHub.address,
|
||||
LENS_HUB_NFT_NAME,
|
||||
testWallet.address,
|
||||
userAddress,
|
||||
true,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.permitForAll(testWallet.address, userAddress, true, {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.transferFrom(testWallet.address, userAddress, FIRST_PROFILE_ID)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.transferFrom(userAddress, testWallet.address, FIRST_PROFILE_ID)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.transferFrom(testWallet.address, userAddress, FIRST_PROFILE_ID)
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('TestWallet should sign burnWithSig, user should submit and burn NFT', async function () {
|
||||
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getBurnWithSigparts(
|
||||
lensHub.address,
|
||||
LENS_HUB_NFT_NAME,
|
||||
FIRST_PROFILE_ID,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensHub.connect(user).burnWithSig(FIRST_PROFILE_ID, { v, r, s, deadline: MAX_UINT256 })
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
157
test/other/deployment-validation.spec.ts
Normal file
157
test/other/deployment-validation.spec.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import {
|
||||
FeeFollowModule__factory,
|
||||
LensHub__factory,
|
||||
ModuleGlobals__factory,
|
||||
TimedFeeCollectModule__factory,
|
||||
TransparentUpgradeableProxy__factory,
|
||||
} from '../../typechain-types';
|
||||
import { ZERO_ADDRESS } from '../helpers/constants';
|
||||
import { ERRORS } from '../helpers/errors';
|
||||
import {
|
||||
BPS_MAX,
|
||||
deployer,
|
||||
deployerAddress,
|
||||
governanceAddress,
|
||||
hubLibs,
|
||||
lensHub,
|
||||
lensHubImpl,
|
||||
LENS_HUB_NFT_NAME,
|
||||
LENS_HUB_NFT_SYMBOL,
|
||||
makeSuiteCleanRoom,
|
||||
moduleGlobals,
|
||||
treasuryAddress,
|
||||
TREASURY_FEE_BPS,
|
||||
user,
|
||||
userAddress,
|
||||
} from '../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('deployment validation', () => {
|
||||
it('Deployer should not be able to initialize implementation due to address(this) check', async function () {
|
||||
await expect(
|
||||
lensHubImpl.initialize(LENS_HUB_NFT_NAME, LENS_HUB_NFT_SYMBOL, governanceAddress)
|
||||
).to.be.revertedWith(ERRORS.CANNOT_INIT_IMPL);
|
||||
});
|
||||
|
||||
it("User should fail to initialize lensHub proxy after it's already been initialized via the proxy constructor", async function () {
|
||||
// Initialization happens in __setup.spec.ts
|
||||
await expect(
|
||||
lensHub.connect(user).initialize('name', 'symbol', userAddress)
|
||||
).to.be.revertedWith(ERRORS.INITIALIZED);
|
||||
});
|
||||
|
||||
it('Deployer should deploy an LensHub implementation, a proxy, initialize it, and fail to initialize it again', async function () {
|
||||
const newImpl = await new LensHub__factory(hubLibs, deployer).deploy(
|
||||
ZERO_ADDRESS,
|
||||
ZERO_ADDRESS
|
||||
);
|
||||
|
||||
let data = newImpl.interface.encodeFunctionData('initialize', [
|
||||
LENS_HUB_NFT_NAME,
|
||||
LENS_HUB_NFT_SYMBOL,
|
||||
governanceAddress,
|
||||
]);
|
||||
|
||||
const proxy = await new TransparentUpgradeableProxy__factory(deployer).deploy(
|
||||
newImpl.address,
|
||||
deployerAddress,
|
||||
data
|
||||
);
|
||||
|
||||
await expect(
|
||||
LensHub__factory.connect(proxy.address, user).initialize('name', 'symbol', userAddress)
|
||||
).to.be.revertedWith(ERRORS.INITIALIZED);
|
||||
});
|
||||
|
||||
it('User should not be able to call admin-only functions on proxy (should fallback) since deployer is admin', async function () {
|
||||
const proxy = TransparentUpgradeableProxy__factory.connect(lensHub.address, user);
|
||||
await expect(proxy.upgradeTo(userAddress)).to.be.revertedWith(ERRORS.NO_SELECTOR);
|
||||
await expect(proxy.upgradeToAndCall(userAddress, [])).to.be.revertedWith(ERRORS.NO_SELECTOR);
|
||||
});
|
||||
|
||||
it('Deployer should be able to call admin-only functions on proxy', async function () {
|
||||
const proxy = TransparentUpgradeableProxy__factory.connect(lensHub.address, deployer);
|
||||
const newImpl = await new LensHub__factory(hubLibs, deployer).deploy(
|
||||
ZERO_ADDRESS,
|
||||
ZERO_ADDRESS
|
||||
);
|
||||
await expect(proxy.upgradeTo(newImpl.address)).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('Deployer should transfer admin to user, deployer should fail to call admin-only functions, user should call admin-only functions', async function () {
|
||||
const proxy = TransparentUpgradeableProxy__factory.connect(lensHub.address, deployer);
|
||||
|
||||
await expect(proxy.changeAdmin(userAddress)).to.not.be.reverted;
|
||||
|
||||
await expect(proxy.upgradeTo(userAddress)).to.be.revertedWith(ERRORS.NO_SELECTOR);
|
||||
await expect(proxy.upgradeToAndCall(userAddress, [])).to.be.revertedWith(ERRORS.NO_SELECTOR);
|
||||
|
||||
const newImpl = await new LensHub__factory(hubLibs, deployer).deploy(
|
||||
ZERO_ADDRESS,
|
||||
ZERO_ADDRESS
|
||||
);
|
||||
|
||||
await expect(proxy.connect(user).upgradeTo(newImpl.address)).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('Should fail to deploy a fee collect module with zero address hub', async function () {
|
||||
await expect(
|
||||
new TimedFeeCollectModule__factory(deployer).deploy(ZERO_ADDRESS, moduleGlobals.address)
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('Should fail to deploy a fee collect module with zero address module globals', async function () {
|
||||
await expect(
|
||||
new TimedFeeCollectModule__factory(deployer).deploy(lensHub.address, ZERO_ADDRESS)
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('Should fail to deploy a fee follow module with zero address hub', async function () {
|
||||
await expect(
|
||||
new FeeFollowModule__factory(deployer).deploy(ZERO_ADDRESS, moduleGlobals.address)
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('Should fail to deploy a fee follow module with zero address module globals', async function () {
|
||||
await expect(
|
||||
new FeeFollowModule__factory(deployer).deploy(lensHub.address, ZERO_ADDRESS)
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('Should fail to deploy module globals with zero address governance', async function () {
|
||||
await expect(
|
||||
new ModuleGlobals__factory(deployer).deploy(ZERO_ADDRESS, treasuryAddress, TREASURY_FEE_BPS)
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('Should fail to deploy module globals with zero address treasury', async function () {
|
||||
await expect(
|
||||
new ModuleGlobals__factory(deployer).deploy(governanceAddress, ZERO_ADDRESS, TREASURY_FEE_BPS)
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('Should fail to deploy module globals with treausury fee > BPS_MAX / 2', async function () {
|
||||
await expect(
|
||||
new ModuleGlobals__factory(deployer).deploy(governanceAddress, treasuryAddress, 5001)
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('Should fail to deploy a fee module with treasury fee equal to or higher than maximum BPS', async function () {
|
||||
await expect(
|
||||
new ModuleGlobals__factory(deployer).deploy(ZERO_ADDRESS, treasuryAddress, BPS_MAX)
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
|
||||
await expect(
|
||||
new ModuleGlobals__factory(deployer).deploy(ZERO_ADDRESS, treasuryAddress, BPS_MAX + 1)
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('Validates LensHub name & symbol', async function () {
|
||||
const name = LENS_HUB_NFT_NAME;
|
||||
const symbol = await lensHub.symbol();
|
||||
|
||||
expect(name).to.eq(LENS_HUB_NFT_NAME);
|
||||
expect(symbol).to.eq(LENS_HUB_NFT_SYMBOL);
|
||||
});
|
||||
});
|
||||
657
test/other/events.spec.ts
Normal file
657
test/other/events.spec.ts
Normal file
@@ -0,0 +1,657 @@
|
||||
import { TransactionReceipt } from '@ethersproject/providers';
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { TransparentUpgradeableProxy__factory } from '../../typechain-types';
|
||||
import { ZERO_ADDRESS } from '../helpers/constants';
|
||||
import {
|
||||
getAbbreviation,
|
||||
getTimestamp,
|
||||
matchEvent,
|
||||
ProtocolState,
|
||||
waitForTx,
|
||||
} from '../helpers/utils';
|
||||
import {
|
||||
approvalFollowModule,
|
||||
deployer,
|
||||
deployerAddress,
|
||||
emptyCollectModule,
|
||||
FIRST_PROFILE_ID,
|
||||
governance,
|
||||
governanceAddress,
|
||||
lensHub,
|
||||
lensHubImpl,
|
||||
LENS_HUB_NFT_NAME,
|
||||
LENS_HUB_NFT_SYMBOL,
|
||||
makeSuiteCleanRoom,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
MOCK_URI,
|
||||
moduleGlobals,
|
||||
treasuryAddress,
|
||||
TREASURY_FEE_BPS,
|
||||
user,
|
||||
userAddress,
|
||||
userTwo,
|
||||
userTwoAddress,
|
||||
} from '../__setup.spec';
|
||||
|
||||
/**
|
||||
* Note: We use the `lensHubImpl` contract to test ERC721 specific events.
|
||||
*
|
||||
* TODO: Add specific test cases to ensure all module encoded return data parameters are
|
||||
* as expected.
|
||||
*
|
||||
* TODO: Add module deployment tests.
|
||||
*/
|
||||
makeSuiteCleanRoom('Events', function () {
|
||||
let receipt: TransactionReceipt;
|
||||
|
||||
context('Misc', function () {
|
||||
it('Proxy initialization should emit expected events', async function () {
|
||||
let data = lensHubImpl.interface.encodeFunctionData('initialize', [
|
||||
LENS_HUB_NFT_NAME,
|
||||
LENS_HUB_NFT_SYMBOL,
|
||||
governanceAddress,
|
||||
]);
|
||||
|
||||
let proxy = await new TransparentUpgradeableProxy__factory(deployer).deploy(
|
||||
lensHubImpl.address,
|
||||
deployerAddress,
|
||||
data
|
||||
);
|
||||
|
||||
receipt = await waitForTx(proxy.deployTransaction, true);
|
||||
|
||||
expect(receipt.logs.length).to.eq(5);
|
||||
matchEvent(receipt, 'Upgraded', [lensHubImpl.address], proxy);
|
||||
matchEvent(receipt, 'AdminChanged', [ZERO_ADDRESS, deployerAddress], proxy);
|
||||
matchEvent(receipt, 'GovernanceSet', [
|
||||
deployerAddress,
|
||||
ZERO_ADDRESS,
|
||||
governanceAddress,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
matchEvent(receipt, 'StateSet', [
|
||||
deployerAddress,
|
||||
ProtocolState.Unpaused,
|
||||
ProtocolState.Paused,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
matchEvent(receipt, 'BaseInitialized', [
|
||||
LENS_HUB_NFT_NAME,
|
||||
LENS_HUB_NFT_SYMBOL,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
context('Hub Governance', function () {
|
||||
it('Governance change should emit expected event', async function () {
|
||||
receipt = await waitForTx(lensHub.connect(governance).setGovernance(userAddress));
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'GovernanceSet', [
|
||||
governanceAddress,
|
||||
governanceAddress,
|
||||
userAddress,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Emergency admin change should emit expected event', async function () {
|
||||
receipt = await waitForTx(lensHub.connect(governance).setEmergencyAdmin(userAddress));
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'EmergencyAdminSet', [
|
||||
governanceAddress,
|
||||
ZERO_ADDRESS,
|
||||
userAddress,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Protocol state change by governance should emit expected event', async function () {
|
||||
receipt = await waitForTx(lensHub.connect(governance).setState(ProtocolState.Paused));
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'StateSet', [
|
||||
governanceAddress,
|
||||
ProtocolState.Unpaused,
|
||||
ProtocolState.Paused,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
|
||||
receipt = await waitForTx(
|
||||
lensHub.connect(governance).setState(ProtocolState.PublishingPaused)
|
||||
);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'StateSet', [
|
||||
governanceAddress,
|
||||
ProtocolState.Paused,
|
||||
ProtocolState.PublishingPaused,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
|
||||
receipt = await waitForTx(lensHub.connect(governance).setState(ProtocolState.Unpaused));
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'StateSet', [
|
||||
governanceAddress,
|
||||
ProtocolState.PublishingPaused,
|
||||
ProtocolState.Unpaused,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Protocol state change by emergency admin should emit expected events', async function () {
|
||||
await waitForTx(lensHub.connect(governance).setEmergencyAdmin(userAddress));
|
||||
receipt = await waitForTx(lensHub.connect(user).setState(ProtocolState.Paused));
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'StateSet', [
|
||||
userAddress,
|
||||
ProtocolState.Unpaused,
|
||||
ProtocolState.Paused,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
|
||||
receipt = await waitForTx(lensHub.connect(user).setState(ProtocolState.PublishingPaused));
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'StateSet', [
|
||||
userAddress,
|
||||
ProtocolState.Paused,
|
||||
ProtocolState.PublishingPaused,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
|
||||
receipt = await waitForTx(lensHub.connect(user).setState(ProtocolState.Unpaused));
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'StateSet', [
|
||||
userAddress,
|
||||
ProtocolState.PublishingPaused,
|
||||
ProtocolState.Unpaused,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Follow module whitelisting functions should emit expected event', async function () {
|
||||
receipt = await waitForTx(
|
||||
lensHub.connect(governance).whitelistFollowModule(userAddress, true)
|
||||
);
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'FollowModuleWhitelisted', [userAddress, true, await getTimestamp()]);
|
||||
|
||||
receipt = await waitForTx(
|
||||
lensHub.connect(governance).whitelistFollowModule(userAddress, false)
|
||||
);
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'FollowModuleWhitelisted', [userAddress, false, await getTimestamp()]);
|
||||
});
|
||||
|
||||
it('Reference module whitelisting functions should emit expected event', async function () {
|
||||
receipt = await waitForTx(
|
||||
lensHub.connect(governance).whitelistReferenceModule(userAddress, true)
|
||||
);
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'ReferenceModuleWhitelisted', [userAddress, true, await getTimestamp()]);
|
||||
|
||||
receipt = await waitForTx(
|
||||
lensHub.connect(governance).whitelistReferenceModule(userAddress, false)
|
||||
);
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'ReferenceModuleWhitelisted', [userAddress, false, await getTimestamp()]);
|
||||
});
|
||||
|
||||
it('Collect module whitelisting functions should emit expected event', async function () {
|
||||
receipt = await waitForTx(
|
||||
lensHub.connect(governance).whitelistCollectModule(userAddress, true)
|
||||
);
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'CollectModuleWhitelisted', [userAddress, true, await getTimestamp()]);
|
||||
|
||||
receipt = await waitForTx(
|
||||
lensHub.connect(governance).whitelistCollectModule(userAddress, false)
|
||||
);
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'CollectModuleWhitelisted', [userAddress, false, await getTimestamp()]);
|
||||
});
|
||||
});
|
||||
|
||||
context('Hub Interaction', function () {
|
||||
async function createProfile() {
|
||||
await waitForTx(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
it('Profile creation for other user should emit the correct events', async function () {
|
||||
receipt = await waitForTx(
|
||||
lensHub.createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
);
|
||||
|
||||
expect(receipt.logs.length).to.eq(2);
|
||||
matchEvent(
|
||||
receipt,
|
||||
'Transfer',
|
||||
[ZERO_ADDRESS, userTwoAddress, FIRST_PROFILE_ID],
|
||||
lensHubImpl
|
||||
);
|
||||
matchEvent(receipt, 'ProfileCreated', [
|
||||
FIRST_PROFILE_ID,
|
||||
userAddress,
|
||||
userTwoAddress,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
ZERO_ADDRESS,
|
||||
[],
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Profile creation should emit the correct events', async function () {
|
||||
receipt = await waitForTx(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
);
|
||||
|
||||
expect(receipt.logs.length).to.eq(2);
|
||||
matchEvent(receipt, 'Transfer', [ZERO_ADDRESS, userAddress, FIRST_PROFILE_ID], lensHubImpl);
|
||||
matchEvent(receipt, 'ProfileCreated', [
|
||||
FIRST_PROFILE_ID,
|
||||
userAddress,
|
||||
userAddress,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
ZERO_ADDRESS,
|
||||
[],
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Setting follow module should emit correct events', async function () {
|
||||
await createProfile();
|
||||
|
||||
await waitForTx(
|
||||
lensHub.connect(governance).whitelistFollowModule(approvalFollowModule.address, true)
|
||||
);
|
||||
|
||||
receipt = await waitForTx(
|
||||
lensHub.setFollowModule(FIRST_PROFILE_ID, approvalFollowModule.address, [])
|
||||
);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'FollowModuleSet', [
|
||||
FIRST_PROFILE_ID,
|
||||
approvalFollowModule.address,
|
||||
[],
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Setting dispatcher should emit correct events', async function () {
|
||||
await createProfile();
|
||||
|
||||
receipt = await waitForTx(lensHub.setDispatcher(FIRST_PROFILE_ID, userAddress));
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'DispatcherSet', [FIRST_PROFILE_ID, userAddress, await getTimestamp()]);
|
||||
});
|
||||
|
||||
it('Posting should emit the correct events', async function () {
|
||||
await createProfile();
|
||||
|
||||
await waitForTx(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
);
|
||||
|
||||
receipt = await waitForTx(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'PostCreated', [
|
||||
FIRST_PROFILE_ID,
|
||||
1,
|
||||
MOCK_URI,
|
||||
emptyCollectModule.address,
|
||||
[],
|
||||
ZERO_ADDRESS,
|
||||
[],
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Commenting should emit the correct events', async function () {
|
||||
await createProfile();
|
||||
|
||||
await waitForTx(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
);
|
||||
await waitForTx(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
);
|
||||
|
||||
receipt = await waitForTx(
|
||||
lensHub.comment({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
|
||||
matchEvent(receipt, 'CommentCreated', [
|
||||
FIRST_PROFILE_ID,
|
||||
2,
|
||||
MOCK_URI,
|
||||
FIRST_PROFILE_ID,
|
||||
1,
|
||||
emptyCollectModule.address,
|
||||
[],
|
||||
ZERO_ADDRESS,
|
||||
[],
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Mirroring should emit the correct events', async function () {
|
||||
await createProfile();
|
||||
|
||||
await waitForTx(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
);
|
||||
await waitForTx(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
);
|
||||
|
||||
receipt = await waitForTx(
|
||||
lensHub.mirror({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
|
||||
matchEvent(receipt, 'MirrorCreated', [
|
||||
FIRST_PROFILE_ID,
|
||||
2,
|
||||
FIRST_PROFILE_ID,
|
||||
1,
|
||||
ZERO_ADDRESS,
|
||||
[],
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Following should emit correct events', async function () {
|
||||
await createProfile();
|
||||
|
||||
await waitForTx(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
);
|
||||
|
||||
receipt = await waitForTx(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]]));
|
||||
const followNFT = await lensHub.getFollowNFT(FIRST_PROFILE_ID);
|
||||
|
||||
const expectedName = MOCK_PROFILE_HANDLE + '-Follower';
|
||||
const expectedSymbol = getAbbreviation(MOCK_PROFILE_HANDLE) + '-Fl';
|
||||
|
||||
expect(receipt.logs.length).to.eq(6);
|
||||
matchEvent(receipt, 'FollowNFTDeployed', [FIRST_PROFILE_ID, followNFT, await getTimestamp()]);
|
||||
matchEvent(receipt, 'Followed', [userTwoAddress, [FIRST_PROFILE_ID], await getTimestamp()]);
|
||||
matchEvent(receipt, 'BaseInitialized', [expectedName, expectedSymbol, await getTimestamp()]);
|
||||
matchEvent(receipt, 'Transfer', [ZERO_ADDRESS, userTwoAddress, 1], lensHubImpl);
|
||||
matchEvent(receipt, 'FollowNFTTransferred', [
|
||||
FIRST_PROFILE_ID,
|
||||
1,
|
||||
ZERO_ADDRESS,
|
||||
userTwoAddress,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Collecting should emit correct events', async function () {
|
||||
await createProfile();
|
||||
|
||||
await waitForTx(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
);
|
||||
|
||||
await waitForTx(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
);
|
||||
|
||||
await waitForTx(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]]));
|
||||
|
||||
receipt = await waitForTx(lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, []));
|
||||
const collectNFT = await lensHub.getCollectNFT(FIRST_PROFILE_ID, 1);
|
||||
const expectedName = MOCK_PROFILE_HANDLE + '-Collect-' + '1';
|
||||
const expectedSymbol = getAbbreviation(MOCK_PROFILE_HANDLE) + '-Cl-' + '1';
|
||||
|
||||
expect(receipt.logs.length).to.eq(6);
|
||||
matchEvent(receipt, 'CollectNFTDeployed', [
|
||||
FIRST_PROFILE_ID,
|
||||
1,
|
||||
collectNFT,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
matchEvent(receipt, 'Collected', [
|
||||
userTwoAddress,
|
||||
FIRST_PROFILE_ID,
|
||||
1,
|
||||
FIRST_PROFILE_ID,
|
||||
1,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
matchEvent(receipt, 'BaseInitialized', [expectedName, expectedSymbol, await getTimestamp()]);
|
||||
matchEvent(receipt, 'CollectNFTInitialized', [FIRST_PROFILE_ID, 1, await getTimestamp()]);
|
||||
matchEvent(receipt, 'Transfer', [ZERO_ADDRESS, userTwoAddress, 1], lensHubImpl);
|
||||
matchEvent(receipt, 'CollectNFTTransferred', [
|
||||
FIRST_PROFILE_ID,
|
||||
1,
|
||||
1,
|
||||
ZERO_ADDRESS,
|
||||
userTwoAddress,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Collecting from a mirror should emit correct events', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await createProfile();
|
||||
|
||||
await waitForTx(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
);
|
||||
|
||||
await waitForTx(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
);
|
||||
|
||||
await waitForTx(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]]));
|
||||
|
||||
await waitForTx(
|
||||
lensHub.connect(userTwo).createProfile({
|
||||
to: userTwoAddress,
|
||||
handle: 'usertwo',
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
);
|
||||
|
||||
await waitForTx(
|
||||
lensHub.connect(userTwo).mirror({
|
||||
profileId: secondProfileId,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
);
|
||||
|
||||
receipt = await waitForTx(lensHub.connect(userTwo).collect(secondProfileId, 1, []));
|
||||
const collectNFT = await lensHub.getCollectNFT(FIRST_PROFILE_ID, 1);
|
||||
const expectedName = MOCK_PROFILE_HANDLE + '-Collect-' + '1';
|
||||
const expectedSymbol = getAbbreviation(MOCK_PROFILE_HANDLE) + '-Cl-' + '1';
|
||||
|
||||
expect(receipt.logs.length).to.eq(6);
|
||||
matchEvent(receipt, 'CollectNFTDeployed', [
|
||||
FIRST_PROFILE_ID,
|
||||
1,
|
||||
collectNFT,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
matchEvent(receipt, 'Collected', [
|
||||
userTwoAddress,
|
||||
secondProfileId,
|
||||
1,
|
||||
FIRST_PROFILE_ID,
|
||||
1,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
matchEvent(receipt, 'BaseInitialized', [expectedName, expectedSymbol, await getTimestamp()]);
|
||||
matchEvent(receipt, 'CollectNFTInitialized', [FIRST_PROFILE_ID, 1, await getTimestamp()]);
|
||||
matchEvent(receipt, 'Transfer', [ZERO_ADDRESS, userTwoAddress, 1], lensHubImpl);
|
||||
matchEvent(receipt, 'CollectNFTTransferred', [
|
||||
FIRST_PROFILE_ID,
|
||||
1,
|
||||
1,
|
||||
ZERO_ADDRESS,
|
||||
userTwoAddress,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
context('Module Globals Governance', function () {
|
||||
it('Governance change should emit expected event', async function () {
|
||||
receipt = await waitForTx(moduleGlobals.connect(governance).setGovernance(userAddress));
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'ModuleGlobalsGovernanceSet', [
|
||||
governanceAddress,
|
||||
userAddress,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Treasury change should emit expected event', async function () {
|
||||
receipt = await waitForTx(moduleGlobals.connect(governance).setTreasury(userAddress));
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'ModuleGlobalsTreasurySet', [
|
||||
treasuryAddress,
|
||||
userAddress,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Treasury fee change should emit expected event', async function () {
|
||||
receipt = await waitForTx(moduleGlobals.connect(governance).setTreasuryFee(123));
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'ModuleGlobalsTreasuryFeeSet', [
|
||||
TREASURY_FEE_BPS,
|
||||
123,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Currency whitelisting should emit expected event', async function () {
|
||||
receipt = await waitForTx(
|
||||
moduleGlobals.connect(governance).whitelistCurrency(userAddress, true)
|
||||
);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'ModuleGlobalsCurrencyWhitelisted', [
|
||||
userAddress,
|
||||
false,
|
||||
true,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
|
||||
receipt = await waitForTx(
|
||||
moduleGlobals.connect(governance).whitelistCurrency(userAddress, false)
|
||||
);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'ModuleGlobalsCurrencyWhitelisted', [
|
||||
userAddress,
|
||||
true,
|
||||
false,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
642
test/other/misc.spec.ts
Normal file
642
test/other/misc.spec.ts
Normal file
@@ -0,0 +1,642 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { ZERO_ADDRESS } from '../helpers/constants';
|
||||
import { ERRORS } from '../helpers/errors';
|
||||
import {
|
||||
approvalFollowModule,
|
||||
emptyCollectModule,
|
||||
FIRST_PROFILE_ID,
|
||||
followerOnlyReferenceModule,
|
||||
governance,
|
||||
governanceAddress,
|
||||
lensHub,
|
||||
makeSuiteCleanRoom,
|
||||
mockFollowModule,
|
||||
mockModuleData,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
MOCK_PROFILE_URI,
|
||||
MOCK_URI,
|
||||
moduleGlobals,
|
||||
OTHER_MOCK_URI,
|
||||
timedFeeCollectModule,
|
||||
treasuryAddress,
|
||||
TREASURY_FEE_BPS,
|
||||
user,
|
||||
userAddress,
|
||||
userTwo,
|
||||
userTwoAddress,
|
||||
} from '../__setup.spec';
|
||||
|
||||
/**
|
||||
* @dev Some of these tests may be redundant, but are still present to ensure an isolated environment,
|
||||
* in particular if other test files are changed.
|
||||
*/
|
||||
makeSuiteCleanRoom('Misc', function () {
|
||||
context('NFT Transfer Emitters', function () {
|
||||
it('User should not be able to call the follow NFT transfer event emitter function', async function () {
|
||||
await expect(
|
||||
lensHub.emitFollowNFTTransferEvent(FIRST_PROFILE_ID, 1, userAddress, userTwoAddress)
|
||||
).to.be.revertedWith(ERRORS.NOT_FOLLOW_NFT);
|
||||
});
|
||||
|
||||
it('User should not be able to call the collect NFT transfer event emitter function', async function () {
|
||||
await expect(
|
||||
lensHub.emitCollectNFTTransferEvent(FIRST_PROFILE_ID, 1, 1, userAddress, userTwoAddress)
|
||||
).to.be.revertedWith(ERRORS.NOT_COLLECT_NFT);
|
||||
});
|
||||
});
|
||||
|
||||
context('Lens Hub Misc', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('UserTwo should fail to burn profile owned by user without being approved', async function () {
|
||||
await expect(lensHub.connect(userTwo).burn(FIRST_PROFILE_ID)).to.be.revertedWith(
|
||||
ERRORS.NOT_OWNER_OR_APPROVED
|
||||
);
|
||||
});
|
||||
|
||||
it('User should burn profile owned by user', async function () {
|
||||
await expect(lensHub.burn(FIRST_PROFILE_ID)).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('UserTwo should burn profile owned by user if approved', async function () {
|
||||
await expect(lensHub.approve(userTwoAddress, FIRST_PROFILE_ID)).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).burn(FIRST_PROFILE_ID)).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('Governance getter should return proper address', async function () {
|
||||
expect(await lensHub.getGovernance()).to.eq(governanceAddress);
|
||||
});
|
||||
|
||||
it('Profile handle getter should return the correct handle', async function () {
|
||||
expect(await lensHub.getHandle(FIRST_PROFILE_ID)).to.eq(MOCK_PROFILE_HANDLE);
|
||||
});
|
||||
|
||||
it('Profile dispatcher getter should return the zero address when no dispatcher is set', async function () {
|
||||
expect(await lensHub.getDispatcher(FIRST_PROFILE_ID)).to.eq(ZERO_ADDRESS);
|
||||
});
|
||||
|
||||
it('Profile creator whitelist getter should return expected values', async function () {
|
||||
expect(await lensHub.isProfileCreatorWhitelisted(userAddress)).to.eq(true);
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistProfileCreator(userAddress, false)
|
||||
).to.not.be.reverted;
|
||||
expect(await lensHub.isProfileCreatorWhitelisted(userAddress)).to.eq(false);
|
||||
});
|
||||
|
||||
it('Profile dispatcher getter should return the correct dispatcher address when it is set, then zero after it is transferred', async function () {
|
||||
await expect(lensHub.setDispatcher(FIRST_PROFILE_ID, userTwoAddress)).to.not.be.reverted;
|
||||
expect(await lensHub.getDispatcher(FIRST_PROFILE_ID)).to.eq(userTwoAddress);
|
||||
|
||||
await expect(
|
||||
lensHub.transferFrom(userAddress, userTwoAddress, FIRST_PROFILE_ID)
|
||||
).to.not.be.reverted;
|
||||
expect(await lensHub.getDispatcher(FIRST_PROFILE_ID)).to.eq(ZERO_ADDRESS);
|
||||
});
|
||||
|
||||
it('Profile follow NFT getter should return the zero address before the first follow, then the correct address afterwards', async function () {
|
||||
expect(await lensHub.getFollowNFT(FIRST_PROFILE_ID)).to.eq(ZERO_ADDRESS);
|
||||
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
expect(await lensHub.getFollowNFT(FIRST_PROFILE_ID)).to.not.eq(ZERO_ADDRESS);
|
||||
});
|
||||
|
||||
it('Profile follow module getter should return the zero address, then the correct follow module after it is set', async function () {
|
||||
expect(await lensHub.getFollowModule(FIRST_PROFILE_ID)).to.eq(ZERO_ADDRESS);
|
||||
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistFollowModule(mockFollowModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.setFollowModule(FIRST_PROFILE_ID, mockFollowModule.address, mockModuleData)
|
||||
).to.not.be.reverted;
|
||||
expect(await lensHub.getFollowModule(FIRST_PROFILE_ID)).to.eq(mockFollowModule.address);
|
||||
});
|
||||
|
||||
it('Profile publication count getter should return zero, then the correct amount after some publications', async function () {
|
||||
expect(await lensHub.getPubCount(FIRST_PROFILE_ID)).to.eq(0);
|
||||
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
const expectedCount = 5;
|
||||
for (let i = 0; i < expectedCount; i++) {
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
}
|
||||
expect(await lensHub.getPubCount(FIRST_PROFILE_ID)).to.eq(expectedCount);
|
||||
});
|
||||
|
||||
it('Profile tokenURI should return the accurate URI', async function () {
|
||||
expect(await lensHub.tokenURI(FIRST_PROFILE_ID)).to.eq(MOCK_PROFILE_URI);
|
||||
});
|
||||
|
||||
it('Publication reference module getter should return the correct reference module (or zero in case of no reference module)', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub
|
||||
.connect(governance)
|
||||
.whitelistReferenceModule(followerOnlyReferenceModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
expect(await lensHub.getReferenceModule(FIRST_PROFILE_ID, 1)).to.eq(ZERO_ADDRESS);
|
||||
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: followerOnlyReferenceModule.address,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
expect(await lensHub.getReferenceModule(FIRST_PROFILE_ID, 2)).to.eq(
|
||||
followerOnlyReferenceModule.address
|
||||
);
|
||||
});
|
||||
|
||||
it('Publication pointer getter should return an empty pointer for posts', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const pointer = await lensHub.getPubPointer(FIRST_PROFILE_ID, 1);
|
||||
expect(pointer[0]).to.eq(0);
|
||||
expect(pointer[1]).to.eq(0);
|
||||
});
|
||||
|
||||
it('Publication pointer getter should return the correct pointer for comments', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.comment({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const pointer = await lensHub.getPubPointer(FIRST_PROFILE_ID, 2);
|
||||
expect(pointer[0]).to.eq(FIRST_PROFILE_ID);
|
||||
expect(pointer[1]).to.eq(1);
|
||||
});
|
||||
|
||||
it('Publication pointer getter should return the correct pointer for mirrors', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.mirror({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
const pointer = await lensHub.getPubPointer(FIRST_PROFILE_ID, 2);
|
||||
expect(pointer[0]).to.eq(FIRST_PROFILE_ID);
|
||||
expect(pointer[1]).to.eq(1);
|
||||
});
|
||||
|
||||
it('Publication content URI getter should return the correct URI for posts', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
expect(await lensHub.getContentURI(FIRST_PROFILE_ID, 1)).to.eq(MOCK_URI);
|
||||
});
|
||||
|
||||
it('Publication content URI getter should return the correct URI for comments', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.comment({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: OTHER_MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
expect(await lensHub.getContentURI(FIRST_PROFILE_ID, 2)).to.eq(OTHER_MOCK_URI);
|
||||
});
|
||||
|
||||
it('Publication content URI getter should return the correct URI for mirrors', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.mirror({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
expect(await lensHub.getContentURI(FIRST_PROFILE_ID, 2)).to.eq(MOCK_URI);
|
||||
});
|
||||
|
||||
it('Publication collect module getter should return the correct collectModule for posts', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
expect(await lensHub.getCollectModule(FIRST_PROFILE_ID, 1)).to.eq(emptyCollectModule.address);
|
||||
});
|
||||
|
||||
it('Publication collect module getter should return the correct collectModule for comments', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.mirror({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.comment({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: OTHER_MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 2,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
expect(await lensHub.getCollectModule(FIRST_PROFILE_ID, 3)).to.eq(emptyCollectModule.address);
|
||||
});
|
||||
|
||||
it('Publication collect module getter should return the zero address for mirrors', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.mirror({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
expect(await lensHub.getCollectModule(FIRST_PROFILE_ID, 2)).to.eq(ZERO_ADDRESS);
|
||||
});
|
||||
|
||||
it('Publication type getter should return the correct publication type for all publication types, or nonexistent', async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistCollectModule(emptyCollectModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.post({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: MOCK_URI,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.comment({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
contentURI: OTHER_MOCK_URI,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
collectModule: emptyCollectModule.address,
|
||||
collectModuleData: [],
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.mirror({
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
profileIdPointed: FIRST_PROFILE_ID,
|
||||
pubIdPointed: 1,
|
||||
referenceModule: ZERO_ADDRESS,
|
||||
referenceModuleData: [],
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
|
||||
expect(await lensHub.getPubType(FIRST_PROFILE_ID, 1)).to.eq(0);
|
||||
expect(await lensHub.getPubType(FIRST_PROFILE_ID, 2)).to.eq(1);
|
||||
expect(await lensHub.getPubType(FIRST_PROFILE_ID, 3)).to.eq(2);
|
||||
expect(await lensHub.getPubType(FIRST_PROFILE_ID, 4)).to.eq(3);
|
||||
});
|
||||
|
||||
it('Profile getter should return accurate profile parameters', async function () {
|
||||
const fetchedProfile = await lensHub.getProfile(FIRST_PROFILE_ID);
|
||||
expect(fetchedProfile.pubCount).to.eq(0);
|
||||
expect(fetchedProfile.handle).to.eq(MOCK_PROFILE_HANDLE);
|
||||
expect(fetchedProfile.followModule).to.eq(ZERO_ADDRESS);
|
||||
expect(fetchedProfile.followNFT).to.eq(ZERO_ADDRESS);
|
||||
});
|
||||
});
|
||||
|
||||
context('Follow Module Misc', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.connect(governance).whitelistFollowModule(approvalFollowModule.address, true)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: approvalFollowModule.address,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('User should fail to call processFollow directly on a follow module inheriting from the FollowValidatorFollowModuleBase', async function () {
|
||||
await expect(approvalFollowModule.processFollow(ZERO_ADDRESS, 0, [])).to.be.revertedWith(
|
||||
ERRORS.NOT_HUB
|
||||
);
|
||||
});
|
||||
|
||||
it('Follow module follow validation when there are no follows, and thus no deployed Follow NFT should revert', async function () {
|
||||
await expect(
|
||||
approvalFollowModule.validateFollow(FIRST_PROFILE_ID, userTwoAddress, 0)
|
||||
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
|
||||
});
|
||||
|
||||
it('Follow module follow validation with zero ID input should revert after another address follows, but not the queried address', async function () {
|
||||
await expect(
|
||||
approvalFollowModule.connect(user).approve(FIRST_PROFILE_ID, [userAddress], [true])
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
approvalFollowModule.validateFollow(FIRST_PROFILE_ID, userTwoAddress, 0)
|
||||
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
|
||||
});
|
||||
|
||||
it('Follow module follow validation with specific ID input should revert after following, but the specific ID does not exist yet', async function () {
|
||||
await expect(
|
||||
approvalFollowModule.connect(user).approve(FIRST_PROFILE_ID, [userAddress], [true])
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
approvalFollowModule.validateFollow(FIRST_PROFILE_ID, userAddress, 2)
|
||||
).to.be.revertedWith(ERRORS.ERC721_QUERY_FOR_NONEXISTENT_TOKEN);
|
||||
});
|
||||
|
||||
it('Follow module follow validation with specific ID input should revert if another address owns the specified follow NFT', async function () {
|
||||
await expect(
|
||||
approvalFollowModule.connect(user).approve(FIRST_PROFILE_ID, [userAddress], [true])
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
approvalFollowModule.validateFollow(FIRST_PROFILE_ID, userTwoAddress, 1)
|
||||
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
|
||||
});
|
||||
|
||||
it('Follow module follow validation with specific ID input should work if the queried address owns the specified follow NFT', async function () {
|
||||
await expect(
|
||||
approvalFollowModule.connect(user).approve(FIRST_PROFILE_ID, [userAddress], [true])
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
approvalFollowModule.validateFollow(FIRST_PROFILE_ID, userAddress, 1)
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
});
|
||||
|
||||
context('Collect Module Misc', function () {
|
||||
it('Should fail to call processCollect directly on a collect module inheriting from the FollowValidationModuleBase contract', async function () {
|
||||
await expect(
|
||||
timedFeeCollectModule.processCollect(0, ZERO_ADDRESS, 0, 0, [])
|
||||
).to.be.revertedWith(ERRORS.NOT_HUB);
|
||||
});
|
||||
});
|
||||
|
||||
context('Module Globals', function () {
|
||||
context('Negatives', function () {
|
||||
it('User should fail to set the governance address on the module globals', async function () {
|
||||
await expect(moduleGlobals.connect(user).setGovernance(ZERO_ADDRESS)).to.be.revertedWith(
|
||||
ERRORS.NOT_GOVERNANCE
|
||||
);
|
||||
});
|
||||
|
||||
it('User should fail to set the treasury on the module globals', async function () {
|
||||
await expect(moduleGlobals.connect(user).setTreasury(ZERO_ADDRESS)).to.be.revertedWith(
|
||||
ERRORS.NOT_GOVERNANCE
|
||||
);
|
||||
});
|
||||
|
||||
it('User should fail to set the treasury fee on the module globals', async function () {
|
||||
await expect(moduleGlobals.connect(user).setTreasuryFee(0)).to.be.revertedWith(
|
||||
ERRORS.NOT_GOVERNANCE
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('Governance should set the governance address on the module globals', async function () {
|
||||
await expect(
|
||||
moduleGlobals.connect(governance).setGovernance(userAddress)
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('Governance should set the treasury on the module globals', async function () {
|
||||
await expect(moduleGlobals.connect(governance).setTreasury(userAddress)).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('Governance should set the treasury fee on the module globals', async function () {
|
||||
await expect(moduleGlobals.connect(governance).setTreasuryFee(0)).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('Governance should fail to whitelist the zero address as a currency', async function () {
|
||||
await expect(
|
||||
moduleGlobals.connect(governance).whitelistCurrency(ZERO_ADDRESS, true)
|
||||
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
|
||||
});
|
||||
|
||||
it('Governance getter should return expected address', async function () {
|
||||
expect(await moduleGlobals.getGovernance()).to.eq(governanceAddress);
|
||||
});
|
||||
|
||||
it('Treasury getter should return expected address', async function () {
|
||||
expect(await moduleGlobals.getTreasury()).to.eq(treasuryAddress);
|
||||
});
|
||||
|
||||
it('Treasury fee getter should return the expected fee', async function () {
|
||||
expect(await moduleGlobals.getTreasuryFee()).to.eq(TREASURY_FEE_BPS);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
53
test/other/upgradeability.spec.ts
Normal file
53
test/other/upgradeability.spec.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { ethers } from 'hardhat';
|
||||
import {
|
||||
MockLensHubV2BadRevision__factory,
|
||||
MockLensHubV2__factory,
|
||||
TransparentUpgradeableProxy__factory,
|
||||
} from '../../typechain-types';
|
||||
import { ERRORS } from '../helpers/errors';
|
||||
import { abiCoder, deployer, lensHub, makeSuiteCleanRoom, user } from '../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Upgradeability', function () {
|
||||
const valueToSet = 123;
|
||||
|
||||
it('Should fail to initialize an implementation with the same revision', async function () {
|
||||
const newImpl = await new MockLensHubV2BadRevision__factory(deployer).deploy();
|
||||
const proxyHub = TransparentUpgradeableProxy__factory.connect(lensHub.address, deployer);
|
||||
const hub = MockLensHubV2BadRevision__factory.connect(proxyHub.address, user);
|
||||
await expect(proxyHub.upgradeTo(newImpl.address)).to.not.be.reverted;
|
||||
await expect(hub.initialize(valueToSet)).to.be.revertedWith(ERRORS.INITIALIZED);
|
||||
});
|
||||
|
||||
// The LensHub contract's last storage variable by default is at the 23nd slot (index 22) and contains the emergency admin
|
||||
// We're going to validate the first 23 slots and the 24rd slot before and after the change
|
||||
it("Should upgrade and set a new variable's value, previous storage is unchanged, new value is accurate", async function () {
|
||||
const newImpl = await new MockLensHubV2__factory(deployer).deploy();
|
||||
const proxyHub = TransparentUpgradeableProxy__factory.connect(lensHub.address, deployer);
|
||||
|
||||
let prevStorage: string[] = [];
|
||||
for (let i = 0; i < 23; i++) {
|
||||
const valueAt = await ethers.provider.getStorageAt(proxyHub.address, i);
|
||||
prevStorage.push(valueAt);
|
||||
}
|
||||
|
||||
let prevNextSlot = await ethers.provider.getStorageAt(proxyHub.address, 23);
|
||||
const formattedZero = abiCoder.encode(['uint256'], [0]);
|
||||
expect(prevNextSlot).to.eq(formattedZero);
|
||||
|
||||
await proxyHub.upgradeTo(newImpl.address);
|
||||
await expect(
|
||||
MockLensHubV2__factory.connect(proxyHub.address, user).setAdditionalValue(valueToSet)
|
||||
).to.not.be.reverted;
|
||||
|
||||
for (let i = 0; i < 23; i++) {
|
||||
const valueAt = await ethers.provider.getStorageAt(proxyHub.address, i);
|
||||
expect(valueAt).to.eq(prevStorage[i]);
|
||||
}
|
||||
|
||||
const newNextSlot = await ethers.provider.getStorageAt(proxyHub.address, 23);
|
||||
const formattedValue = abiCoder.encode(['uint256'], [valueToSet]);
|
||||
expect(newNextSlot).to.eq(formattedValue);
|
||||
});
|
||||
});
|
||||
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2019",
|
||||
"module": "commonjs",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"outDir": "dist",
|
||||
"noImplicitAny": false,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["./scripts", "./test"],
|
||||
"files": ["./hardhat.config.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user