Merge pull request #39 from lens-protocol/feat/foundry

Feat/foundry
This commit is contained in:
Victor Naumik
2023-01-31 19:24:24 +01:00
committed by GitHub
197 changed files with 27673 additions and 24757 deletions

View File

@@ -1 +0,0 @@
node_modules/

View File

@@ -20,4 +20,37 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Compile code and run test coverage
run: npm run hardhat:coverage
foundry:
strategy:
fail-fast: true
name: Foundry Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: actions/setup-node@v3
with:
node-version: 16
- name: Install dependencies
run: npm ci
- name: Install LCOV
run: sudo apt-get -y install lcov
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Run Forge build
run: |
forge --version
forge build
- name: Run Forge tests
run: |
cp .env.example .env
source .env
forge test -vvv
- name: Run Full Merged Coverage
run: npm run coverage

16
.gitignore vendored
View File

@@ -17,12 +17,24 @@ build/
/tasks/collect.ts
/tasks/test-module.ts
/coverage
/coverage*
coverage.json
.coverage_artifacts
.coverage_cache
.coverage_contracts
lcov*
addresses.json
contracts/core/modules/follow/SecretCodeFollowModule.sol
# Compiler files
forge-cache/
out/
# Ignores development broadcast logs
!/broadcast
/broadcast/*/31337/
/broadcast/**/dry-run/
contracts/core/modules/follow/SecretCodeFollowModule.sol
.DS_Store

0
.gitmodules vendored Normal file
View File

View File

@@ -1,21 +0,0 @@
# syntax=docker/dockerfile:1.3
FROM ethereum/solc:0.8.7 as build-deps
FROM node:16 as build-packages
COPY package*.json ./
COPY tsconfig*.json ./
RUN npm ci --quiet
FROM node:16
WORKDIR /src
COPY --from=build-deps /usr/bin/solc /usr/bin/solc
COPY --from=build-packages /node_modules /node_modules
COPY docker-entrypoint.sh /docker-entrypoint.sh
USER node
ENTRYPOINT ["sh", "/docker-entrypoint.sh"]

526
TestsList.md Normal file
View File

@@ -0,0 +1,526 @@
Collecting
Generic
Negatives
// TODO: Move to only follows module test?
[?] User two should fail to collect without being a follower
[?] User two should follow, then transfer the followNFT and fail to collect
[X] User two should fail to collect a nonexistent publication
Scenarios
// TODO: Move to only follows module test?
[?] Collecting should work if the collector is the publication owner even when he is not following himself and follow NFT was not deployed
[?] Collecting should work if the collector is the publication owner even when he is not following himself and follow NFT was deployed
[ ] Should return the expected token IDs when collecting publications
[ ] UserTwo should follow, then collect, receive a collect NFT with the expected properties
[X] UserTwo should follow, then mirror, then collect on their mirror, receive a collect NFT with expected properties
[ ] UserTwo should follow, then mirror, mirror their mirror then collect on their latest mirror, receive a collect NFT with expected properties
Meta-tx
Negatives
[X] TestWallet should fail to collect with sig with signature deadline mismatch
[X] TestWallet should fail to collect with sig with invalid deadline
[X] TestWallet should fail to collect with sig with invalid nonce
[?] TestWallet should fail to collect with sig without being a follower
[ ] TestWallet should sign attempt to collect with sig, cancel via empty permitForAll, fail to collect with sig
Scenarios
// TODO: Move to only follows module test?
[?] TestWallet should follow, then collect with sig, receive a collect NFT with expected properties
[X] TestWallet should follow, mirror, then collect with sig on their mirror
Following
Generic
Negatives
[X] UserTwo should fail to follow a nonexistent profile
[X] UserTwo should fail to follow with array mismatch
[X] UserTwo should fail to follow a profile that has been burned
[X] UserTwo should fail to follow profile with id 0
Scenarios
[X] UserTwo should follow profile 1, receive a followNFT with ID 1, followNFT properties should be correct
[-] UserTwo should follow profile 1 twice, receiving followNFTs with IDs 1 and 2
[-] UserTwo should follow profile 1 3 times in the same call, receive IDs 1,2 and 3
[X] Should return the expected token IDs when following profiles
Meta-tx
Negatives
[X] TestWallet should fail to follow with sig with signature deadline mismatch
[X] TestWallet should fail to follow with sig with invalid deadline
[X] TestWallet should fail to follow with sig with invalid nonce
[X] TestWallet should fail to follow a nonexistent profile with sig
[-] TestWallet should sign attempt to follow with sig, cancel with empty permitForAll, then fail to follow with sig
Scenarios
[X] TestWallet should follow profile 1 with sig, receive a follow NFT with ID 1, follow NFT name and symbol should be correct
[X] TestWallet should follow profile 1 with sig twice in the same call, receive follow NFTs with IDs 1 and 2
Governance Functions
Negatives
[X] User should not be able to call governance functions
Scenarios
[X] Governance should successfully whitelist and unwhitelist modules
[X] Governance should successfully change the governance address
Multi-State Hub
Common
Negatives
[X] User should fail to set the state on the hub
[X] User should fail to set the emergency admin
[X] Governance should set user as emergency admin, user should fail to set protocol state to Unpaused
[X] Governance should set user as emergency admin, user should fail to set protocol state to PublishingPaused or Paused from Paused
Scenarios
[X] Governance should set user as emergency admin, user sets protocol state but fails to set emergency admin, governance sets emergency admin to the zero address, user fails to set protocol state
[X] Governance should set the protocol state, fetched protocol state should be accurate
[X] Governance should set user as emergency admin, user should set protocol state to PublishingPaused, then Paused, then fail to set it to PublishingPaused
Paused State
Scenarios
[X] User should create a profile, governance should pause the hub, transferring the profile should fail
[X] Governance should pause the hub, profile creation should fail, then governance unpauses the hub and profile creation should work
[X] Governance should pause the hub, setting follow module should fail, then governance unpauses the hub and setting follow module should work
[X] Governance should pause the hub, setting follow module with sig should fail, then governance unpauses the hub and setting follow module with sig should work
// Replaced dispatcher with DelegatedExecutor for the following two tests:
[X] Governance should pause the hub, setting dispatcher should fail, then governance unpauses the hub and setting dispatcher should work
[X] Governance should pause the hub, setting dispatcher with sig should fail, then governance unpauses the hub and setting dispatcher with sig should work
[X] Governance should pause the hub, setting profile URI should fail, then governance unpauses the hub and setting profile URI should work
[X] Governance should pause the hub, setting profile URI with sig should fail, then governance unpauses the hub and setting profile URI should work
[X] Governance should pause the hub, setting follow NFT URI should fail, then governance unpauses the hub and setting follow NFT URI should work
[X] Governance should pause the hub, setting follow NFT URI with sig should fail, then governance unpauses the hub and setting follow NFT URI should work
[X] Governance should pause the hub, posting should fail, then governance unpauses the hub and posting should work
[X] Governance should pause the hub, posting with sig should fail, then governance unpauses the hub and posting with sig should work
[X] Governance should pause the hub, commenting should fail, then governance unpauses the hub and commenting should work
[X] Governance should pause the hub, commenting with sig should fail, then governance unpauses the hub and commenting with sig should work
[X] Governance should pause the hub, mirroring should fail, then governance unpauses the hub and mirroring should work
[X] Governance should pause the hub, mirroring with sig should fail, then governance unpauses the hub and mirroring with sig should work
[X] Governance should pause the hub, burning should fail, then governance unpauses the hub and burning should work
[X] Governance should pause the hub, following should fail, then governance unpauses the hub and following should work
[X] Governance should pause the hub, following with sig should fail, then governance unpauses the hub and following with sig should work
[X] Governance should pause the hub, collecting should fail, then governance unpauses the hub and collecting should work
[X] Governance should pause the hub, collecting with sig should fail, then governance unpauses the hub and collecting with sig should work
PublishingPaused State
Scenarios
[X] Governance should pause publishing, profile creation should work
[X] Governance should pause publishing, setting follow module should work
[X] Governance should pause publishing, setting follow module with sig should work
[X] Governance should pause publishing, setting dispatcher should work
[X] Governance should pause publishing, setting dispatcher with sig should work
[X] Governance should pause publishing, setting profile URI should work
[X] Governance should pause publishing, setting profile URI with sig should work
[X] Governance should pause publishing, posting should fail, then governance unpauses the hub and posting should work
[X] Governance should pause publishing, posting with sig should fail, then governance unpauses the hub and posting with sig should work
[X] Governance should pause publishing, commenting should fail, then governance unpauses the hub and commenting should work
[X] Governance should pause publishing, commenting with sig should fail, then governance unpauses the hub and commenting with sig should work
[X] Governance should pause publishing, mirroring should fail, then governance unpauses the hub and mirroring should work
[X] Governance should pause publishing, mirroring with sig should fail, then governance unpauses the hub and mirroring with sig should work
[X] Governance should pause publishing, burning should work
[X] Governance should pause publishing, following should work
[X] Governance should pause publishing, following with sig should work
[X] Governance should pause publishing, collecting should work
[X] Governance should pause publishing, collecting with sig should work
Publishing Comments
Generic
Negatives
[X] UserTwo should fail to publish a comment to a profile owned by User
[X] User should fail to comment with an unwhitelisted collect module
[X] User should fail to comment with an unwhitelisted reference module
[-] (Module Tests) User should fail to comment with invalid collect module data format
[-] (Module Tests) User should fail to comment with invalid reference module data format
[X] User should fail to comment on a publication that does not exist
[X] User should fail to comment on the same comment they are creating (pubId = 2, commentCeption)
Scenarios
[X] User should create a comment with empty collect module data, reference module, and reference module data, fetched comment data should be accurate
[X] Should return the expected token IDs when commenting publications
[X] User should create a post using the mock reference module as reference module, then comment on that post
Meta-tx
Negatives
[X] Testwallet should fail to comment with sig with signature deadline mismatch
[X] Testwallet should fail to comment with sig with invalid deadline
[X] Testwallet should fail to comment with sig with invalid nonce
[X] Testwallet should fail to comment with sig with unwhitelisted collect module
[X] TestWallet should fail to comment with sig with unwhitelisted reference module
[X] TestWallet should fail to comment with sig on a publication that does not exist
[X] TestWallet should fail to comment with sig on the comment they are creating (commentCeption)
[X] TestWallet should sign attempt to comment with sig, cancel via empty permitForAll, then fail to comment with sig
Scenarios
[X] TestWallet should comment with sig, fetched comment data should be accurate
Publishing mirrors
Generic
Negatives
[X] UserTwo should fail to publish a mirror to a profile owned by User
[X] User should fail to mirror with an unwhitelisted reference module
[-] (Module Tests) User should fail to mirror with invalid reference module data format
[X] User should fail to mirror a publication that does not exist
Scenarios
[X] Should return the expected token IDs when mirroring publications
[X] User should create a mirror with empty reference module and reference module data, fetched mirror data should be accurate
[X] 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
[X] User should create a post using the mock reference module as reference module, then mirror that post
Meta-tx
Negatives
[X] Testwallet should fail to mirror with sig with signature deadline mismatch
[X] Testwallet should fail to mirror with sig with invalid deadline
[X] Testwallet should fail to mirror with sig with invalid nonce
[X] Testwallet should fail to mirror with sig with unwhitelisted reference module
[X] TestWallet should fail to mirror a publication with sig that does not exist yet
[X] TestWallet should sign attempt to mirror with sig, cancel via empty permitForAll, then fail to mirror with sig
Scenarios
[X] Testwallet should mirror with sig, fetched mirror data should be accurate
[X] TestWallet should mirror a mirror with sig, fetched mirror data should be accurate
Publishing Posts
Generic
Negatives
[X] UserTwo should fail to post to a profile owned by User
[X] User should fail to post with an unwhitelisted collect module
[X] User should fail to post with an unwhitelisted reference module
[-] (Modules tests) User should fail to post with invalid collect module data format
[-] (Modules Tests) User should fail to post with invalid reference module data format
Scenarios
[X] Should return the expected token IDs when ~~mirroring~~ posting publications
[X] User should create a post with empty collect and reference module data, fetched post data should be accurate
[X] User should create a post with a whitelisted collect and reference module
Meta-tx
Negatives
[X] Testwallet should fail to post with sig with signature deadline mismatch
[X] Testwallet should fail to post with sig with invalid deadline
[X] Testwallet should fail to post with sig with invalid nonce
[X] Testwallet should fail to post with sig with an unwhitelisted collect module
[X] Testwallet should fail to post with sig with an unwhitelisted reference module
[X] (Replaced it with another post with same nonce) TestWallet should sign attempt to post with sig, cancel via empty permitForAll, then fail to post with sig
[ ] TestWallet should deploy bad EIP1271 implementer, transfer profile to it, then fail to post with sig
Scenarios
[X] TestWallet should post with sig, fetched post data should be accurate
[ ] TestWallet should deploy EIP1271 implementer, transfer profile to it, then post with sig
Default profile Functionality
Generic
Negatives
[X] UserTwo should fail to set the default profile as a profile owned by user 1
Scenarios
[X] User should set the default profile
[X] User should set the default profile and then be able to unset it
[X] User should set the default profile and then be able to change it to another
[X] User should set the default profile and then transfer it, their default profile should be unset
Meta-tx
Negatives
[X] TestWallet should fail to set default profile with sig with signature deadline mismatch
[X] TestWallet should fail to set default profile with sig with invalid deadline
[X] TestWallet should fail to set default profile with sig with invalid nonce
<!-- Already tested in invalid nonce test above -->
[-] TestWallet should sign attempt to set default profile with sig, cancel with empty permitForAll, then fail to set default profile with sig
Scenarios
[X] TestWallet should set the default profile with sig
[X] TestWallet should set the default profile with sig and then be able to unset it
[X] TestWallet should set the default profile and then be able to change it to another
Dispatcher Functionality
Generic
Negatives
[ ] UserTwo should fail to set dispatcher on profile owned by user 1
[ ] UserTwo should fail to publish on profile owned by user 1 without being a dispatcher
Scenarios
[ ] User should set user two as a dispatcher on their profile, user two should post, comment and mirror
Meta-tx
Negatives
[ ] TestWallet should fail to set dispatcher with sig with signature deadline mismatch
[ ] TestWallet should fail to set dispatcher with sig with invalid deadline
[ ] TestWallet should fail to set dispatcher with sig with invalid nonce
[ ] TestWallet should sign attempt to set dispatcher with sig, cancel via empty permitForAll, fail to set dispatcher with sig
Scenarios
[ ] TestWallet should set user two as dispatcher for their profile, user two should post, comment and mirror
Profile Creation
Generic
Negatives
[ ] User should fail to create a profile with a handle longer than 31 bytes
[ ] User should fail to create a profile with an empty handle (0 length bytes)
[ ] User should fail to create a profile with a handle with a capital letter
[ ] User should fail to create a profile with a handle with an invalid character
[ ] User should fail to create a profile with a unwhitelisted follow module
[ ] User should fail to create a profile with with invalid follow module data format
[ ] User should fail to create a profile when they are not a whitelisted profile creator
[ ] User should fail to create a profile with invalid image URI length
Scenarios
[ ] 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
[ ] Should return the expected token IDs when creating profiles
[ ] User should be able to create a profile with a handle including "-" and "\_" characters
[ ] 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
[ ] User should be able to create a profile with a whitelisted follow module
[ ] User should create a profile for userTwo
Profile URI Functionality
Generic
Negatives
[ ] UserTwo should fail to set the profile URI on profile owned by user 1
[ ] UserTwo should fail to set the profile URI on profile owned by user 1
[ ] UserTwo should fail to change the follow NFT URI for profile one
Scenarios
[ ] User should have a custom image tokenURI after setting the profile imageURI
[ ] User should set a custom image URI under 32 bytes of length, profile image URI should be accurate
[ ] Default image should be used when no imageURI set
[ ] Default image should be used when imageURI contains double-quotes
[ ] Should return the correct tokenURI after transfer
[ ] Should return the correct tokenURI after a follow
[ ] User should set user two as a dispatcher on their profile, user two should set the profile URI
[ ] User should follow profile 1, user should change the follow NFT URI, URI is accurate before and after the change
Meta-tx
Negatives
[ ] TestWallet should fail to set profile URI with sig with signature deadline mismatch
[ ] TestWallet should fail to set profile URI with sig with invalid deadline
[ ] TestWallet should fail to set profile URI with sig with invalid nonce
[ ] TestWallet should sign attempt to set profile URI with sig, cancel with empty permitForAll, then fail to set profile URI with sig
[ ] TestWallet should fail to set the follow NFT URI with sig with signature deadline mismatch
[ ] TestWallet should fail to set the follow NFT URI with sig with invalid deadline
[ ] TestWallet should fail to set the follow NFT URI with sig with invalid nonce
[ ] TestWallet should sign attempt to set follow NFT URI with sig, cancel with empty permitForAll, then fail to set follow NFT URI with sig
Scenarios
[ ] TestWallet should set the profile URI with sig
[ ] TestWallet should set the follow NFT URI with sig
Setting Follow Module
Generic
Negatives
[X] UserTwo should fail to set the follow module for the profile owned by User
[X] User should fail to set a follow module that is not whitelisted
[X] User should fail to set a follow module with invalid follow module data format
Scenarios
[X] 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
Meta-tx
Negatives
[X] TestWallet should fail to set a follow module with sig with signature deadline mismatch
[X] TestWallet should fail to set a follow module with sig with invalid deadline
[X] TestWallet should fail to set a follow module with sig with invalid nonce
[X] TestWallet should fail to set a follow module with sig with an unwhitelisted follow module
[X] TestWallet should sign attempt to set follow module with sig, then cancel with empty permitForAll, then fail to set follow module with sig
Scenarios
[X] TestWallet should set a whitelisted follow module with sig, fetching the profile follow module should return the correct address
Collect NFT
Negatives
[ ] User should fail to reinitialize the collect NFT
[ ] User should fail to mint on the collect NFT
[ ] UserTwo should fail to burn user's collect NFT
[ ] User should fail to get the URI for a token that does not exist
[ ] User should fail to change the royalty percentage if he is not the owner of the publication
[ ] User should fail to change the royalty percentage if the value passed exceeds the royalty basis points
Scenarios
[ ] Collect NFT URI should be valid
[ ] Collect NFT source publication pointer should be accurate
[ ] User should burn their collect NFT
[ ] Default royalties are set to 10%
[ ] User should be able to change the royalties if owns the profile and passes a valid royalty percentage in basis points
[ ] User should be able to get the royalty info even over a token that does not exist yet
[ ] Publication owner should be able to remove royalties by setting them as zero
[ ] If the profile authoring the publication is transferred the royalty info now returns the new owner as recipient
Follow NFT
generic
Negatives
[ ] User should follow, and fail to re-initialize the follow NFT
[-] User should follow, userTwo should fail to burn user's follow NFT
[-] User should follow, then fail to mint a follow NFT directly
[-] User should follow, then fail to get the power at a future block
[-] user should follow, then fail to get the URI for a token that does not exist
Scenarios
[-] User should follow, then burn their follow NFT, governance power is zero before and after
[-] User should follow, delegate to themself, governance power should be zero before the last block, and 1 at the current block
[-] User and userTwo should follow, governance power should be zero, then users delegate multiple times, governance power should be accurate throughout
[-] 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
[-] user and userTwo should follow, user delegates to userTwo twice, governance power should be accurate
[-] 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
[-] user should follow, then get the URI for their token, URI should be accurate
meta-tx
negatives
[-] TestWallet should fail to delegate with sig with signature deadline mismatch
[-] TestWallet should fail to delegate with sig with invalid deadline
[-] TestWallet should fail to delegate with sig with invalid nonce
[-] TestWallet should sign attempt to delegate by sig, cancel with empty permitForAll, then fail to delegate by sig
Scenarios
[-] TestWallet should delegate by sig to user, governance power should be accurate before and after
Lens NFT Base Functionality
generic
[ ] Domain separator fetched from contract should be accurate
meta-tx
Negatives
[ ] TestWallet should fail to permit with zero spender
[ ] TestWallet should fail to permit with invalid token ID
[ ] TestWallet should fail to permit with signature deadline mismatch
[ ] TestWallet should fail to permit with invalid deadline
[ ] TestWallet should fail to permit with invalid nonce
[ ] TestWallet should sign attempt to permit, cancel with empty permitForAll, then fail to permit
[ ] TestWallet should fail to permitForAll with zero spender
[ ] TestWallet should fail to permitForAll with signature deadline mismatch
[ ] TestWallet should fail to permitForAll with invalid deadline
[ ] TestWallet should fail to permitForAll with invalid nonce
[ ] TestWallet should sign attempt to permitForAll, cancel with empty permitForAll, then fail to permitForAll
[ ] TestWallet should fail to burnWithSig with invalid token ID
[ ] TestWallet should fail to burnWithSig with signature deadline mismatch
[ ] TestWallet should fail to burnWithSig with invalid deadline
[ ] TestWallet should fail to burnWithSig with invalid nonce
[ ] TestWallet should sign attempt to burnWithSig, cancel with empty permitForAll, then fail to burnWithSig
[ ] TestWallet should deploy bad EIP1271 implementer, transfer NFT to it, sign message and permit user, permit should fail with invalid sig
Scenarios
[ ] TestWallet should permit user, user should transfer NFT, send back NFT and fail to transfer it again
[ ] TestWallet should permitForAll user, user should transfer NFT, send back NFT and transfer it again
[ ] TestWallet should sign burnWithSig, user should submit and burn NFT
[ ] TestWallet should deploy EIP1271 implementer, transfer NFT to it, sign message and permit user, user should transfer NFT, send back NFT and fail to transfer it again
deployment validation
[ ] Should fail to deploy a LensHub implementation with zero address follow NFT impl
[ ] Should fail to deploy a LensHub implementation with zero address collect NFT impl
[ ] Should fail to deploy a FollowNFT implementation with zero address hub
[ ] Should fail to deploy a CollectNFT implementation with zero address hub
[ ] Deployer should not be able to initialize implementation due to address(this) check
[ ] User should fail to initialize lensHub proxy after it's already been initialized via the proxy constructor
[ ] Deployer should deploy a LensHub implementation, a proxy, initialize it, and fail to initialize it again
[ ] User should not be able to call admin-only functions on proxy (should fallback) since deployer is admin
[ ] Deployer should be able to call admin-only functions on proxy
[ ] Deployer should transfer admin to user, deployer should fail to call admin-only functions, user should call admin-only functions
[ ] Should fail to deploy a fee collect module with zero address hub
[ ] Should fail to deploy a fee collect module with zero address module globals
[ ] Should fail to deploy a fee follow module with zero address hub
[ ] Should fail to deploy a fee follow module with zero address module globals
[ ] Should fail to deploy module globals with zero address governance
[ ] Should fail to deploy module globals with zero address treasury
[ ] Should fail to deploy module globals with treausury fee > BPS_MAX / 2
[ ] Should fail to deploy a fee module with treasury fee equal to or higher than maximum BPS
[ ] Validates LensHub name & symbol
Events
Misc
[X] Proxy initialization should emit expected events
Hub Governance
[X] Governance change should emit expected event
[X] Emergency admin change should emit expected event
[X] Protocol state change by governance should emit expected event
[X] Protocol state change by emergency admin should emit expected events
[X] Follow module whitelisting functions should emit expected event
[X] Reference module whitelisting functions should emit expected event
[X] Collect module whitelisting functions should emit expected event
Hub Interaction
[X] Profile creation for other user should emit the correct events
[X] Profile creation should emit the correct events
[X] Setting follow module should emit correct events
[X] Setting dispatcher should emit correct events
[X] Posting should emit the correct events
[X] Commenting should emit the correct events
[X] Mirroring should emit the correct events
[X] Following should emit correct events
[X] Collecting should emit correct events
[X] Collecting from a mirror should emit correct events
Module Globals Governance
[X] Governance change should emit expected event
[X] Treasury change should emit expected event
[X] Treasury fee change should emit expected event
[X] Currency whitelisting should emit expected event
Misc
NFT Transfer Emitters
[X] User should not be able to call the follow NFT transfer event emitter function
[X] User should not be able to call the collect NFT transfer event emitter function
Lens Hub Misc
[ ] UserTwo should fail to burn profile owned by user without being approved
[ ] User should burn profile owned by user
[ ] UserTwo should burn profile owned by user if approved
[ ] Governance getter should return proper address
[ ] Profile handle getter should return the correct handle
[ ] Profile dispatcher getter should return the zero address when no dispatcher is set
[ ] Profile creator whitelist getter should return expected values
[ ] Profile dispatcher getter should return the correct dispatcher address when it is set, then zero after it is transferred
[ ] Profile follow NFT getter should return the zero address before the first follow, then the correct address afterwards
[ ] Profile follow module getter should return the zero address, then the correct follow module after it is set
[ ] Profile publication count getter should return zero, then the correct amount after some publications
[ ] Follow NFT impl getter should return the correct address
[ ] Collect NFT impl getter should return the correct address
[ ] Profile tokenURI should return the accurate URI
[ ] Publication reference module getter should return the correct reference module (or zero in case of no reference module)
[ ] Publication pointer getter should return an empty pointer for posts
[ ] Publication pointer getter should return the correct pointer for comments
[ ] Publication pointer getter should return the correct pointer for mirrors
[ ] Publication content URI getter should return the correct URI for posts
[ ] Publication content URI getter should return the correct URI for comments
[ ] Publication content URI getter should return the correct URI for mirrors
[ ] Publication collect module getter should return the correct collectModule for posts
[ ] Publication collect module getter should return the correct collectModule for comments
[ ] Publication collect module getter should return the zero address for mirrors
[ ] Publication type getter should return the correct publication type for all publication types, or nonexistent
[ ] Profile getter should return accurate profile parameters
Follow Module Misc
[ ] User should fail to call processFollow directly on a follow module inheriting from the FollowValidatorFollowModuleBase
[ ] Follow module following check when there are no follows, and thus no deployed Follow NFT should return false
[ ] Follow module following check with zero ID input should return false after another address follows, but not the queried address
[ ] Follow module following check with specific ID input should revert after following, but the specific ID does not exist yet
[ ] Follow module following check with specific ID input should return false if another address owns the specified follow NFT
[ ] Follow module following check with specific ID input should return true if the queried address owns the specified follow NFT
Collect Module Misc
[ ] Should fail to call processCollect directly on a collect module inheriting from the FollowValidationModuleBase contract
Module Globals
Negatives
[X] User should fail to set the governance address on the module globals
[X] User should fail to set the treasury on the module globals
[X] User should fail to set the treasury fee on the module globals
Scenarios
[X] Governance should set the governance address on the module globals
[X] Governance should set the treasury on the module globals
[X] Governance should set the treasury fee on the module globals
[X] Governance should fail to whitelist the zero address as a currency
[X] Governance getter should return expected address
[X] Treasury getter should return expected address
[X] Treasury fee getter should return the expected fee
UI Data Provider
[ ] UI Data Provider should return expected values
LensPeriphery
ToggleFollowing
Generic
Negatives
[ ] UserTwo should fail to toggle follow with an incorrect profileId
[ ] UserTwo should fail to toggle follow with array mismatch
[ ] UserTwo should fail to toggle follow from a profile that has been burned
[ ] UserTwo should fail to toggle follow for a followNFT that is not owned by them
Scenarios
[ ] UserTwo should toggle follow with true value, correct event should be emitted
[ ] User should create another profile, userTwo follows, then toggles both, one true, one false, correct event should be emitted
[ ] UserTwo should toggle follow with false value, correct event should be emitted
Meta-tx
Negatives
[ ] TestWallet should fail to toggle follow with sig with signature deadline mismatch
[ ] TestWallet should fail to toggle follow with sig with invalid deadline
[ ] TestWallet should fail to toggle follow with sig with invalid nonce
[ ] TestWallet should fail to toggle follow a nonexistent profile with sig
Scenarios
[ ] TestWallet should toggle follow profile 1 to true with sig, correct event should be emitted
[ ] TestWallet should toggle follow profile 1 to false with sig, correct event should be emitted
// TODO: The whole section is questionable (removed in foundry branch)
Profile Metadata URI
Generic
Negatives
[X] User two should fail to set profile metadata URI for a profile that is not theirs while they are not the dispatcher
Scenarios
[X] User should set user two as dispatcher, user two should set profile metadata URI for user one's profile, fetched data should be accurate
[X] Setting profile metadata should emit the correct event
[X] Setting profile metadata via dispatcher should emit the correct event
Meta-tx
Negatives
[X] TestWallet should fail to set profile metadata URI with sig with signature deadline mismatch
[X] TestWallet should fail to set profile metadata URI with sig with invalid deadline
[X] TestWallet should fail to set profile metadata URI with sig with invalid nonce
Scenarios
[X] TestWallet should set profile metadata URI with sig, fetched data should be accurate and correct event should be emitted
Mock Profile Creation Proxy
Negatives
[ ] Should fail to create profile if handle length before suffix does not reach minimum length
[ ] Should fail to create profile if handle contains an invalid character before the suffix
[ ] Should fail to create profile if handle starts with a dash, underscore or period
Scenarios
[ ] Should be able to create a profile using the whitelisted proxy, received NFT should be valid
Profile Creation Proxy
Negatives
[ ] Should fail to create profile if handle length before suffix does not reach minimum length
[ ] Should fail to create profile if handle contains an invalid character before the suffix
[ ] Should fail to create profile if handle starts with a dash, underscore or period
Scenarios
[ ] Should be able to create a profile using the whitelisted proxy, received NFT should be valid
Upgradeability
[ ] Should fail to initialize an implementation with the same revision
[ ] Should upgrade and set a new variable's value, previous storage is unchanged, new value is accurate

View File

@@ -1,14 +1,15 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {ERC2981CollectionRoyalties} from './base/ERC2981CollectionRoyalties.sol';
import {ERC721Enumerable} from './base/ERC721Enumerable.sol';
import {Errors} from '../libraries/Errors.sol';
import {Events} from '../libraries/Events.sol';
import {ICollectNFT} from '../interfaces/ICollectNFT.sol';
import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.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';
import {ERC721Enumerable} from './base/ERC721Enumerable.sol';
/**
* @title CollectNFT
@@ -17,7 +18,7 @@ import {ERC721Enumerable} from './base/ERC721Enumerable.sol';
* @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 LensNFTBase, ICollectNFT {
contract CollectNFT is LensNFTBase, ERC2981CollectionRoyalties, ICollectNFT {
address public immutable HUB;
uint256 internal _profileId;
@@ -26,11 +27,7 @@ contract CollectNFT is LensNFTBase, ICollectNFT {
bool private _initialized;
uint256 internal _royaltyBasisPoints;
// bytes4(keccak256('royaltyInfo(uint256,uint256)')) == 0x2a55205a
bytes4 internal constant INTERFACE_ID_ERC2981 = 0x2a55205a;
uint16 internal constant BASIS_POINTS = 10000;
uint256 internal _royaltiesInBasisPoints;
// We create the CollectNFT with the pre-computed HUB address before deploying the hub proxy in order
// to initialize the hub proxy at construction.
@@ -49,7 +46,7 @@ contract CollectNFT is LensNFTBase, ICollectNFT {
) external override {
if (_initialized) revert Errors.Initialized();
_initialized = true;
_royaltyBasisPoints = 1000; // 10% of royalties
_setRoyalty(1000); // 10% of royalties
_profileId = profileId;
_pubId = pubId;
super._initialize(name, symbol);
@@ -76,42 +73,6 @@ contract CollectNFT is LensNFTBase, ICollectNFT {
return ILensHub(HUB).getContentURI(_profileId, _pubId);
}
/**
* @notice Changes the royalty percentage for secondary sales. Can only be called publication's
* profile owner.
*
* @param royaltyBasisPoints The royalty percentage meassured in basis points. Each basis point
* represents 0.01%.
*/
function setRoyalty(uint256 royaltyBasisPoints) external {
if (IERC721(HUB).ownerOf(_profileId) == msg.sender) {
if (royaltyBasisPoints > BASIS_POINTS) {
revert Errors.InvalidParameter();
} else {
_royaltyBasisPoints = royaltyBasisPoints;
}
} else {
revert Errors.NotProfileOwner();
}
}
/**
* @notice Called with the sale price to determine how much royalty
* is owed and to whom.
*
* @param tokenId The token ID of the NFT queried for royalty information.
* @param salePrice The sale price of the NFT specified.
* @return A tuple with the address who should receive the royalties and the royalty
* payment amount for the given sale price.
*/
function royaltyInfo(uint256 tokenId, uint256 salePrice)
external
view
returns (address, uint256)
{
return (IERC721(HUB).ownerOf(_profileId), (salePrice * _royaltyBasisPoints) / BASIS_POINTS);
}
/**
* @dev See {IERC165-supportsInterface}.
*/
@@ -119,10 +80,30 @@ contract CollectNFT is LensNFTBase, ICollectNFT {
public
view
virtual
override(ERC721Enumerable)
override(ERC2981CollectionRoyalties, ERC721Enumerable)
returns (bool)
{
return interfaceId == INTERFACE_ID_ERC2981 || super.supportsInterface(interfaceId);
return
ERC2981CollectionRoyalties.supportsInterface(interfaceId) ||
ERC721Enumerable.supportsInterface(interfaceId);
}
function _getReceiver(uint256 tokenId) internal view override returns (address) {
return IERC721(HUB).ownerOf(_profileId);
}
function _beforeRoyaltiesSet(uint256 royaltiesInBasisPoints) internal view override {
if (IERC721(HUB).ownerOf(_profileId) != msg.sender) {
revert Errors.NotProfileOwner();
}
}
function _getRoyaltiesInBasisPointsSlot() internal pure override returns (uint256) {
uint256 slot;
assembly {
slot := _royaltiesInBasisPoints.slot
}
return slot;
}
/**

View File

@@ -1,52 +1,39 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {IFollowNFT} from '../interfaces/IFollowNFT.sol';
import {IFollowModule} from '../interfaces/IFollowModule.sol';
import {ILensHub} from '../interfaces/ILensHub.sol';
import '../libraries/Constants.sol';
import {DataTypes} from '../libraries/DataTypes.sol';
import {ERC2981CollectionRoyalties} from './base/ERC2981CollectionRoyalties.sol';
import {ERC721Enumerable} from './base/ERC721Enumerable.sol';
import {Errors} from '../libraries/Errors.sol';
import {Events} from '../libraries/Events.sol';
import {DataTypes} from '../libraries/DataTypes.sol';
import {Constants} from '../libraries/Constants.sol';
import {HubRestricted} from './base/HubRestricted.sol';
import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
import {IERC721Time} from '../interfaces/IERC721Time.sol';
import {IFollowNFT} from '../interfaces/IFollowNFT.sol';
import {ILensHub} from '../interfaces/ILensHub.sol';
import {LensNFTBase} from './base/LensNFTBase.sol';
import {MetaTxHelpers} from '../libraries/helpers/MetaTxHelpers.sol';
import {Strings} from '@openzeppelin/contracts/utils/Strings.sol';
/**
* @title FollowNFT
* @author Lens Protocol
*
* @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.
*
* NOTE: This contract assumes total NFT supply for this follow NFT will never exceed 2^128 - 1
*/
contract FollowNFT is LensNFTBase, IFollowNFT {
struct Snapshot {
uint128 blockNumber;
uint128 value;
}
contract FollowNFT is HubRestricted, LensNFTBase, ERC2981CollectionRoyalties, IFollowNFT {
using Strings for uint256;
address public immutable HUB;
bytes32 internal constant DELEGATE_BY_SIG_TYPEHASH =
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;
mapping(uint256 => Snapshot) internal _delSupplySnapshots;
uint256 internal _delSupplySnapshotCount;
uint256 internal _profileId;
uint256 internal _tokenIdCounter;
uint256[5] ___DEPRECATED_SLOTS; // Deprecated slots, previously used for delegations.
uint256 internal _followedProfileId;
uint256 internal _lastFollowTokenId;
bool private _initialized;
// We create the FollowNFT with the pre-computed HUB address before deploying the hub.
constructor(address hub) {
if (hub == address(0)) revert Errors.InitParamsInvalid();
HUB = hub;
mapping(uint256 => FollowData) internal _followDataByFollowTokenId;
mapping(uint256 => uint256) internal _followTokenIdByFollowerProfileId;
mapping(uint256 => uint256) internal _followApprovalByFollowTokenId;
uint256 internal _royaltiesInBasisPoints;
event FollowApproval(uint256 indexed followerProfileId, uint256 indexed followTokenId);
constructor(address hub) HubRestricted(hub) {
_initialized = true;
}
@@ -54,244 +41,430 @@ contract FollowNFT is LensNFTBase, IFollowNFT {
function initialize(uint256 profileId) external override {
if (_initialized) revert Errors.Initialized();
_initialized = true;
_profileId = profileId;
_followedProfileId = profileId;
_setRoyalty(1000); // 10% of royalties
emit Events.FollowNFTInitialized(profileId, block.timestamp);
}
/// @inheritdoc IFollowNFT
function mint(address to) external override returns (uint256) {
if (msg.sender != HUB) revert Errors.NotHub();
unchecked {
uint256 tokenId = ++_tokenIdCounter;
_mint(to, tokenId);
return tokenId;
function follow(
uint256 followerProfileId,
address executor,
uint256 followTokenId
) external override onlyHub returns (uint256) {
if (_followTokenIdByFollowerProfileId[followerProfileId] != 0) {
revert AlreadyFollowing();
}
if (followTokenId == 0) {
// Fresh follow.
return _followMintingNewToken(followerProfileId);
}
address followTokenOwner = _unsafeOwnerOf(followTokenId);
if (followTokenOwner != address(0)) {
// Provided follow token is wrapped.
return
_followWithWrappedToken({
followerProfileId: followerProfileId,
executor: executor,
followTokenId: followTokenId,
followTokenOwner: followTokenOwner
});
}
uint256 currentFollowerProfileId = _followDataByFollowTokenId[followTokenId]
.followerProfileId;
if (currentFollowerProfileId != 0) {
// Provided follow token is unwrapped.
// It has a follower profile set already, it can only be used to follow if that profile was burnt.
return
_followWithUnwrappedTokenFromBurnedProfile({
followerProfileId: followerProfileId,
followTokenId: followTokenId,
currentFollowerProfileId: currentFollowerProfileId
});
}
// Provided follow token does not exist anymore, it can only be used if profile attempting to follow is
// allowed to recover it.
return
_followByRecoveringToken({
followerProfileId: followerProfileId,
followTokenId: followTokenId
});
}
/// @inheritdoc IFollowNFT
function unfollow(uint256 unfollowerProfileId, address executor) external override onlyHub {
uint256 followTokenId = _followTokenIdByFollowerProfileId[unfollowerProfileId];
if (followTokenId == 0) {
revert NotFollowing();
}
address followTokenOwner = _unsafeOwnerOf(followTokenId);
if (followTokenOwner == address(0)) {
// Follow token is unwrapped.
// Unfollowing and allowing recovery.
_unfollow({unfollower: unfollowerProfileId, followTokenId: followTokenId});
_followDataByFollowTokenId[followTokenId]
.profileIdAllowedToRecover = unfollowerProfileId;
} else {
// Follow token is wrapped.
address unfollowerProfileOwner = IERC721(HUB).ownerOf(unfollowerProfileId);
// Follower profile owner or its approved delegated executor must hold the token or be approved-for-all.
if (
(followTokenOwner != unfollowerProfileOwner) &&
(followTokenOwner != executor) &&
!isApprovedForAll(followTokenOwner, executor) &&
!isApprovedForAll(followTokenOwner, unfollowerProfileOwner)
) {
revert DoesNotHavePermissions();
}
_unfollow({unfollower: unfollowerProfileId, followTokenId: followTokenId});
}
}
/// @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 {
unchecked {
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
DELEGATE_BY_SIG_TYPEHASH,
delegator,
delegatee,
sigNonces[delegator]++,
sig.deadline
)
)
),
delegator,
sig
);
function removeFollower(uint256 followTokenId) external override {
address followTokenOwner = ownerOf(followTokenId);
if (followTokenOwner == msg.sender || isApprovedForAll(followTokenOwner, msg.sender)) {
_unfollowIfHasFollower(followTokenId);
} else {
revert DoesNotHavePermissions();
}
_delegate(delegator, delegatee);
}
/// @inheritdoc IFollowNFT
function getPowerByBlockNumber(address user, uint256 blockNumber)
function approveFollow(uint256 followerProfileId, uint256 followTokenId) external override {
if (!IERC721Time(HUB).exists(followerProfileId)) {
revert Errors.TokenDoesNotExist();
}
address followTokenOwner = _unsafeOwnerOf(followTokenId);
if (followTokenOwner == address(0)) {
revert OnlyWrappedFollowTokens();
}
if (followTokenOwner != msg.sender && !isApprovedForAll(followTokenOwner, msg.sender)) {
revert DoesNotHavePermissions();
}
_approveFollow(followerProfileId, followTokenId);
}
/// @inheritdoc IFollowNFT
function wrap(uint256 followTokenId) external override {
if (_isFollowTokenWrapped(followTokenId)) {
revert AlreadyWrapped();
}
uint256 followerProfileId = _followDataByFollowTokenId[followTokenId].followerProfileId;
address wrappedTokenReceiver;
if (followerProfileId == 0) {
uint256 profileIdAllowedToRecover = _followDataByFollowTokenId[followTokenId]
.profileIdAllowedToRecover;
if (profileIdAllowedToRecover == 0) {
revert FollowTokenDoesNotExist();
}
wrappedTokenReceiver = IERC721(HUB).ownerOf(profileIdAllowedToRecover);
delete _followDataByFollowTokenId[followTokenId].profileIdAllowedToRecover;
} else {
wrappedTokenReceiver = IERC721(HUB).ownerOf(followerProfileId);
}
if (msg.sender != wrappedTokenReceiver) {
revert DoesNotHavePermissions();
}
_mint(wrappedTokenReceiver, followTokenId);
}
/// @inheritdoc IFollowNFT
function unwrap(uint256 followTokenId) external override {
if (_followDataByFollowTokenId[followTokenId].followerProfileId == 0) {
revert NotFollowing();
}
super.burn(followTokenId);
}
/// @inheritdoc IFollowNFT
function processBlock(uint256 followerProfileId) external override onlyHub {
uint256 followTokenId = _followTokenIdByFollowerProfileId[followerProfileId];
if (followTokenId != 0) {
if (!_isFollowTokenWrapped(followTokenId)) {
// Wrap it first, so the user stops following but does not lose the token when being blocked.
_mint(IERC721(HUB).ownerOf(followerProfileId), followTokenId);
}
_unfollow(followerProfileId, followTokenId);
ILensHub(HUB).emitUnfollowedEvent(followerProfileId, _followedProfileId);
}
}
/// @inheritdoc IFollowNFT
function getFollowerProfileId(uint256 followTokenId) external view override returns (uint256) {
return _followDataByFollowTokenId[followTokenId].followerProfileId;
}
/// @inheritdoc IFollowNFT
function isFollowing(uint256 followerProfileId) external view override returns (bool) {
return _followTokenIdByFollowerProfileId[followerProfileId] != 0;
}
/// @inheritdoc IFollowNFT
function getFollowTokenId(uint256 followerProfileId) external view override returns (uint256) {
return _followTokenIdByFollowerProfileId[followerProfileId];
}
/// @inheritdoc IFollowNFT
function getOriginalFollowTimestamp(uint256 followTokenId)
external
view
override
returns (uint256)
{
if (blockNumber > block.number) revert Errors.BlockNumberInvalid();
uint256 snapshotCount = _snapshotCount[user];
if (snapshotCount == 0) return 0; // Returning zero since this means the user never delegated and has no power
return _getSnapshotValueByBlockNumber(_snapshots[user], blockNumber, snapshotCount);
return _followDataByFollowTokenId[followTokenId].originalFollowTimestamp;
}
/// @inheritdoc IFollowNFT
function getDelegatedSupplyByBlockNumber(uint256 blockNumber)
function getFollowTimestamp(uint256 followTokenId) external view override returns (uint256) {
return _followDataByFollowTokenId[followTokenId].followTimestamp;
}
/// @inheritdoc IFollowNFT
function getProfileIdAllowedToRecover(uint256 followTokenId)
external
view
override
returns (uint256)
{
if (blockNumber > block.number) revert Errors.BlockNumberInvalid();
uint256 snapshotCount = _delSupplySnapshotCount;
if (snapshotCount == 0) return 0; // Returning zero since this means a delegation has never occurred
return _getSnapshotValueByBlockNumber(_delSupplySnapshots, blockNumber, snapshotCount);
return _followDataByFollowTokenId[followTokenId].profileIdAllowedToRecover;
}
/// @inheritdoc IFollowNFT
function getFollowData(uint256 followTokenId)
external
view
override
returns (FollowData memory)
{
return _followDataByFollowTokenId[followTokenId];
}
/// @inheritdoc IFollowNFT
function getFollowApproved(uint256 followTokenId) external view override returns (uint256) {
return _followApprovalByFollowTokenId[followTokenId];
}
function burnWithSig(uint256 followTokenId, DataTypes.EIP712Signature calldata sig)
public
override
{
_unfollowIfHasFollower(followTokenId);
super.burnWithSig(followTokenId, sig);
}
function burn(uint256 followTokenId) public override {
_unfollowIfHasFollower(followTokenId);
super.burn(followTokenId);
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(ERC2981CollectionRoyalties, ERC721Enumerable)
returns (bool)
{
return
ERC2981CollectionRoyalties.supportsInterface(interfaceId) ||
ERC721Enumerable.supportsInterface(interfaceId);
}
function name() public view override returns (string memory) {
string memory handle = ILensHub(HUB).getHandle(_profileId);
return string(abi.encodePacked(handle, Constants.FOLLOW_NFT_NAME_SUFFIX));
return string(abi.encodePacked(_followedProfileId.toString(), FOLLOW_NFT_NAME_SUFFIX));
}
function symbol() public view override returns (string memory) {
string memory handle = ILensHub(HUB).getHandle(_profileId);
bytes4 firstBytes = bytes4(bytes(handle));
return string(abi.encodePacked(firstBytes, Constants.FOLLOW_NFT_SYMBOL_SUFFIX));
}
function _getSnapshotValueByBlockNumber(
mapping(uint256 => Snapshot) storage _shots,
uint256 blockNumber,
uint256 snapshotCount
) internal view returns (uint256) {
unchecked {
uint256 lower = 0;
uint256 upper = snapshotCount - 1;
// First check most recent snapshot
if (_shots[upper].blockNumber <= blockNumber) return _shots[upper].value;
// Next check implicit zero balance
if (_shots[lower].blockNumber > blockNumber) return 0;
while (upper > lower) {
uint256 center = upper - (upper - lower) / 2;
Snapshot memory snapshot = _shots[center];
if (snapshot.blockNumber == blockNumber) {
return snapshot.value;
} else if (snapshot.blockNumber < blockNumber) {
lower = center;
} else {
upper = center - 1;
}
}
return _shots[lower].value;
}
return string(abi.encodePacked(_followedProfileId.toString(), FOLLOW_NFT_SYMBOL_SUFFIX));
}
/**
* @dev This returns the follow NFT URI fetched from the hub.
*/
function tokenURI(uint256 tokenId) public view override returns (string memory) {
if (!_exists(tokenId)) revert Errors.TokenDoesNotExist();
return ILensHub(HUB).getFollowNFTURI(_profileId);
function tokenURI(uint256 followTokenId) public view override returns (string memory) {
if (!_exists(followTokenId)) revert Errors.TokenDoesNotExist();
return ILensHub(HUB).getFollowNFTURI(_followedProfileId);
}
function _followMintingNewToken(uint256 followerProfileId) internal returns (uint256) {
uint256 followTokenIdAssigned;
unchecked {
followTokenIdAssigned = ++_lastFollowTokenId;
}
_baseFollow({
followerProfileId: followerProfileId,
followTokenId: followTokenIdAssigned,
isOriginalFollow: true
});
return followTokenIdAssigned;
}
function _followWithWrappedToken(
uint256 followerProfileId,
address executor,
uint256 followTokenId,
address followTokenOwner
) internal returns (uint256) {
bool isFollowApproved = _followApprovalByFollowTokenId[followTokenId] == followerProfileId;
address followerProfileOwner = IERC721(HUB).ownerOf(followerProfileId);
if (
!isFollowApproved &&
followTokenOwner != followerProfileOwner &&
followTokenOwner != executor &&
!isApprovedForAll(followTokenOwner, executor) &&
!isApprovedForAll(followTokenOwner, followerProfileOwner)
) {
revert DoesNotHavePermissions();
}
// The executor is allowed to write the follower in that wrapped token.
if (isFollowApproved) {
// The `_followApprovalByFollowTokenId` was used, now needs to be cleared.
_approveFollow(0, followTokenId);
}
_replaceFollower({
currentFollowerProfileId: _followDataByFollowTokenId[followTokenId].followerProfileId,
newFollowerProfileId: followerProfileId,
followTokenId: followTokenId
});
return followTokenId;
}
function _followWithUnwrappedTokenFromBurnedProfile(
uint256 followerProfileId,
uint256 followTokenId,
uint256 currentFollowerProfileId
) internal returns (uint256) {
if (IERC721Time(HUB).exists(currentFollowerProfileId)) {
revert DoesNotHavePermissions();
}
_replaceFollower({
currentFollowerProfileId: currentFollowerProfileId,
newFollowerProfileId: followerProfileId,
followTokenId: followTokenId
});
return followTokenId;
}
function _followByRecoveringToken(uint256 followerProfileId, uint256 followTokenId)
internal
returns (uint256)
{
if (
_followDataByFollowTokenId[followTokenId].profileIdAllowedToRecover != followerProfileId
) {
revert FollowTokenDoesNotExist();
}
_baseFollow({
followerProfileId: followerProfileId,
followTokenId: followTokenId,
isOriginalFollow: false
});
return followTokenId;
}
function _replaceFollower(
uint256 currentFollowerProfileId,
uint256 newFollowerProfileId,
uint256 followTokenId
) internal {
if (currentFollowerProfileId != 0) {
// As it has a follower, unfollow first, removing current follower.
delete _followTokenIdByFollowerProfileId[currentFollowerProfileId];
ILensHub(HUB).emitUnfollowedEvent(currentFollowerProfileId, _followedProfileId);
}
// Perform the follow, setting new follower.
_baseFollow({
followerProfileId: newFollowerProfileId,
followTokenId: followTokenId,
isOriginalFollow: false
});
}
function _baseFollow(
uint256 followerProfileId,
uint256 followTokenId,
bool isOriginalFollow
) internal {
_followTokenIdByFollowerProfileId[followerProfileId] = followTokenId;
_followDataByFollowTokenId[followTokenId].followerProfileId = uint160(followerProfileId);
_followDataByFollowTokenId[followTokenId].followTimestamp = uint48(block.timestamp);
delete _followDataByFollowTokenId[followTokenId].profileIdAllowedToRecover;
if (isOriginalFollow) {
_followDataByFollowTokenId[followTokenId].originalFollowTimestamp = uint48(
block.timestamp
);
}
}
function _unfollowIfHasFollower(uint256 followTokenId) internal {
uint256 followerProfileId = _followDataByFollowTokenId[followTokenId].followerProfileId;
if (followerProfileId != 0) {
_unfollow(followerProfileId, followTokenId);
ILensHub(HUB).emitUnfollowedEvent(followerProfileId, _followedProfileId);
}
}
function _unfollow(uint256 unfollower, uint256 followTokenId) internal {
delete _followTokenIdByFollowerProfileId[unfollower];
delete _followDataByFollowTokenId[followTokenId].followerProfileId;
delete _followDataByFollowTokenId[followTokenId].followTimestamp;
delete _followDataByFollowTokenId[followTokenId].profileIdAllowedToRecover;
}
function _approveFollow(uint256 approvedProfileId, uint256 followTokenId) internal {
_followApprovalByFollowTokenId[followTokenId] = approvedProfileId;
emit FollowApproval(approvedProfileId, followTokenId);
}
/**
* @dev Upon transfers, we move the appropriate delegations, and emit the transfer event in the hub.
* @dev Upon transfers, we clear follow approvals, and emit the transfer event in the hub.
*/
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId
uint256 followTokenId
) internal override {
address fromDelegatee = _delegates[from];
address toDelegatee = _delegates[to];
address followModule = ILensHub(HUB).getFollowModule(_profileId);
if (from != address(0)) {
// It is cleared on unwrappings and transfers, and it can not be set on unwrapped tokens.
// As a consequence, there is no need to clear it on wrappings.
_approveFollow(0, followTokenId);
}
super._beforeTokenTransfer(from, to, followTokenId);
ILensHub(HUB).emitFollowNFTTransferEvent(_followedProfileId, followTokenId, from, to);
}
_moveDelegate(fromDelegatee, toDelegatee, 1);
function _getReceiver(uint256 followTokenId) internal view override returns (address) {
return IERC721(HUB).ownerOf(_followedProfileId);
}
super._beforeTokenTransfer(from, to, tokenId);
ILensHub(HUB).emitFollowNFTTransferEvent(_profileId, tokenId, from, to);
if (followModule != address(0)) {
IFollowModule(followModule).followModuleTransferHook(_profileId, from, to, tokenId);
function _beforeRoyaltiesSet(uint256 royaltiesInBasisPoints) internal view override {
if (IERC721(HUB).ownerOf(_followedProfileId) != msg.sender) {
revert Errors.NotProfileOwner();
}
}
function _delegate(address delegator, address delegatee) internal {
uint256 delegatorBalance = balanceOf(delegator);
address previousDelegate = _delegates[delegator];
_delegates[delegator] = delegatee;
_moveDelegate(previousDelegate, delegatee, delegatorBalance);
function _isFollowTokenWrapped(uint256 followTokenId) internal view returns (bool) {
return _exists(followTokenId);
}
function _moveDelegate(
address from,
address to,
uint256 amount
) internal {
unchecked {
bool fromZero = from == address(0);
if (!fromZero) {
uint256 fromSnapshotCount = _snapshotCount[from];
// Underflow is impossible since, if from != address(0), then a delegation must have occurred (at least 1 snapshot)
uint256 previous = _snapshots[from][fromSnapshotCount - 1].value;
uint128 newValue = uint128(previous - amount);
_writeSnapshot(from, newValue, fromSnapshotCount);
emit Events.FollowNFTDelegatedPowerChanged(from, newValue, block.timestamp);
}
if (to != address(0)) {
// if from == address(0) then this is an initial delegation (add amount to supply)
if (fromZero) {
// It is expected behavior that the `previousDelSupply` underflows upon the first delegation,
// returning the expected value of zero
uint256 delSupplySnapshotCount = _delSupplySnapshotCount;
uint128 previousDelSupply = _delSupplySnapshots[delSupplySnapshotCount - 1]
.value;
uint128 newDelSupply = uint128(previousDelSupply + amount);
_writeSupplySnapshot(newDelSupply, delSupplySnapshotCount);
}
// It is expected behavior that `previous` underflows upon the first delegation to an address,
// returning the expected value of zero
uint256 toSnapshotCount = _snapshotCount[to];
uint128 previous = _snapshots[to][toSnapshotCount - 1].value;
uint128 newValue = uint128(previous + amount);
_writeSnapshot(to, newValue, toSnapshotCount);
emit Events.FollowNFTDelegatedPowerChanged(to, newValue, block.timestamp);
} else {
// If from != address(0) then this is removing a delegation, otherwise we're dealing with a
// non-delegated burn of tokens and don't need to take any action
if (!fromZero) {
// Upon removing delegation (from != address(0) && to == address(0)), supply calculations cannot
// underflow because if from != address(0), then a delegation must have previously occurred, so
// the snapshot count must be >= 1 and the previous delegated supply must be >= amount
uint256 delSupplySnapshotCount = _delSupplySnapshotCount;
uint128 previousDelSupply = _delSupplySnapshots[delSupplySnapshotCount - 1]
.value;
uint128 newDelSupply = uint128(previousDelSupply - amount);
_writeSupplySnapshot(newDelSupply, delSupplySnapshotCount);
}
}
}
function _followTokenExists(uint256 followTokenId) internal view returns (bool) {
return
_followDataByFollowTokenId[followTokenId].followerProfileId != 0 ||
_isFollowTokenWrapped(followTokenId);
}
function _writeSnapshot(
address owner,
uint128 newValue,
uint256 ownerSnapshotCount
) internal {
unchecked {
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;
}
}
}
function _writeSupplySnapshot(uint128 newValue, uint256 supplySnapshotCount) internal {
unchecked {
uint128 currentBlock = uint128(block.number);
// Doing multiple operations in the same block
if (
supplySnapshotCount != 0 &&
_delSupplySnapshots[supplySnapshotCount - 1].blockNumber == currentBlock
) {
_delSupplySnapshots[supplySnapshotCount - 1].value = newValue;
} else {
_delSupplySnapshots[supplySnapshotCount] = Snapshot(currentBlock, newValue);
_delSupplySnapshotCount = supplySnapshotCount + 1;
}
function _getRoyaltiesInBasisPointsSlot() internal pure override returns (uint256) {
uint256 slot;
assembly {
slot := _royaltiesInBasisPoints.slot
}
return slot;
}
}

View File

@@ -1,16 +1,20 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {IFollowNFT} from '../interfaces/IFollowNFT.sol';
import {ILensNFTBase} from '../interfaces/ILensNFTBase.sol';
import {ILensHub} from '../interfaces/ILensHub.sol';
import {Events} from '../libraries/Events.sol';
import {Helpers} from '../libraries/Helpers.sol';
import {Constants} from '../libraries/Constants.sol';
import {DataTypes} from '../libraries/DataTypes.sol';
import {Errors} from '../libraries/Errors.sol';
import {PublishingLogic} from '../libraries/PublishingLogic.sol';
import {GeneralLib} from '../libraries/GeneralLib.sol';
import {ProfileLib} from '../libraries/ProfileLib.sol';
import {PublishingLib} from '../libraries/PublishingLib.sol';
import {ProfileTokenURILogic} from '../libraries/ProfileTokenURILogic.sol';
import {InteractionLogic} from '../libraries/InteractionLogic.sol';
import '../libraries/Constants.sol';
import {LensNFTBase} from './base/LensNFTBase.sol';
import {LensMultiState} from './base/LensMultiState.sol';
import {LensHubStorage} from './storage/LensHubStorage.sol';
@@ -63,7 +67,7 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
address newGovernance
) external override initializer {
super._initialize(name, symbol);
_setState(DataTypes.ProtocolState.Paused);
GeneralLib.initState(DataTypes.ProtocolState.Paused);
_setGovernance(newGovernance);
}
@@ -78,26 +82,12 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
/// @inheritdoc ILensHub
function setEmergencyAdmin(address newEmergencyAdmin) external override onlyGov {
address prevEmergencyAdmin = _emergencyAdmin;
_emergencyAdmin = newEmergencyAdmin;
emit Events.EmergencyAdminSet(
msg.sender,
prevEmergencyAdmin,
newEmergencyAdmin,
block.timestamp
);
GeneralLib.setEmergencyAdmin(newEmergencyAdmin);
}
/// @inheritdoc ILensHub
function setState(DataTypes.ProtocolState newState) external override {
if (msg.sender == _emergencyAdmin) {
if (newState == DataTypes.ProtocolState.Unpaused)
revert Errors.EmergencyAdminCannotUnpause();
_validateNotPaused();
} else if (msg.sender != _governance) {
revert Errors.NotGovernanceOrEmergencyAdmin();
}
_setState(newState);
GeneralLib.setState(newState);
}
///@inheritdoc ILensHub
@@ -140,6 +130,25 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
/// *****PROFILE OWNER FUNCTIONS*****
/// *********************************
/// @inheritdoc ILensNFTBase
function permit(
address spender,
uint256 tokenId,
DataTypes.EIP712Signature calldata sig
) external override {
GeneralLib.permit(spender, tokenId, sig);
}
/// @inheritdoc ILensNFTBase
function permitForAll(
address owner,
address operator,
bool approved,
DataTypes.EIP712Signature calldata sig
) external override {
GeneralLib.permitForAll(owner, operator, approved, sig);
}
/// @inheritdoc ILensHub
function createProfile(DataTypes.CreateProfileData calldata vars)
external
@@ -147,24 +156,21 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
whenNotPaused
returns (uint256)
{
if (!_profileCreatorWhitelisted[msg.sender]) revert Errors.ProfileCreatorNotWhitelisted();
unchecked {
uint256 profileId = ++_profileCounter;
_mint(vars.to, profileId);
PublishingLogic.createProfile(
vars,
profileId,
_profileIdByHandleHash,
_profileById,
_followModuleWhitelisted
);
ProfileLib.createProfile(vars, profileId);
return profileId;
}
}
/// @inheritdoc ILensHub
function setDefaultProfile(uint256 profileId) external override whenNotPaused {
_setDefaultProfile(msg.sender, profileId);
function setDefaultProfile(address onBehalfOf, uint256 profileId)
external
override
whenNotPaused
{
GeneralLib.setDefaultProfile(onBehalfOf, profileId);
}
/// @inheritdoc ILensHub
@@ -173,24 +179,25 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
override
whenNotPaused
{
unchecked {
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
SET_DEFAULT_PROFILE_WITH_SIG_TYPEHASH,
vars.wallet,
vars.profileId,
sigNonces[vars.wallet]++,
vars.sig.deadline
)
)
),
vars.wallet,
vars.sig
);
_setDefaultProfile(vars.wallet, vars.profileId);
}
GeneralLib.setDefaultProfileWithSig(vars);
}
/// @inheritdoc ILensHub
function setProfileMetadataURI(uint256 profileId, string calldata metadataURI)
external
override
whenNotPaused
{
ProfileLib.setProfileMetadataURI(profileId, metadataURI);
}
/// @inheritdoc ILensHub
function setProfileMetadataURIWithSig(DataTypes.SetProfileMetadataURIWithSigData calldata vars)
external
override
whenNotPaused
{
ProfileLib.setProfileMetadataURIWithSig(vars);
}
/// @inheritdoc ILensHub
@@ -199,14 +206,7 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
address followModule,
bytes calldata followModuleInitData
) external override whenNotPaused {
_validateCallerIsProfileOwner(profileId);
PublishingLogic.setFollowModule(
profileId,
followModule,
followModuleInitData,
_profileById[profileId],
_followModuleWhitelisted
);
ProfileLib.setFollowModule(profileId, followModule, followModuleInitData);
}
/// @inheritdoc ILensHub
@@ -215,32 +215,7 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
override
whenNotPaused
{
address owner = ownerOf(vars.profileId);
unchecked {
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
SET_FOLLOW_MODULE_WITH_SIG_TYPEHASH,
vars.profileId,
vars.followModule,
keccak256(vars.followModuleInitData),
sigNonces[owner]++,
vars.sig.deadline
)
)
),
owner,
vars.sig
);
}
PublishingLogic.setFollowModule(
vars.profileId,
vars.followModule,
vars.followModuleInitData,
_profileById[vars.profileId],
_followModuleWhitelisted
);
ProfileLib.setFollowModuleWithSig(vars);
}
/// @inheritdoc ILensHub
@@ -255,25 +230,23 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
override
whenNotPaused
{
address owner = ownerOf(vars.profileId);
unchecked {
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
SET_DISPATCHER_WITH_SIG_TYPEHASH,
vars.profileId,
vars.dispatcher,
sigNonces[owner]++,
vars.sig.deadline
)
)
),
owner,
vars.sig
);
}
_setDispatcher(vars.profileId, vars.dispatcher);
ProfileLib.setDispatcherWithSig(vars);
}
/// @inheritdoc ILensHub
function setDelegatedExecutorApproval(address executor, bool approved)
external
override
whenNotPaused
{
GeneralLib.setDelegatedExecutorApproval(executor, approved);
}
/// @inheritdoc ILensHub
function setDelegatedExecutorApprovalWithSig(
DataTypes.SetDelegatedExecutorApprovalWithSigData calldata vars
) external override whenNotPaused {
GeneralLib.setDelegatedExecutorApprovalWithSig(vars);
}
/// @inheritdoc ILensHub
@@ -282,8 +255,7 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
override
whenNotPaused
{
_validateCallerIsProfileOwnerOrDispatcher(profileId);
_setProfileImageURI(profileId, imageURI);
ProfileLib.setProfileImageURI(profileId, imageURI);
}
/// @inheritdoc ILensHub
@@ -292,25 +264,7 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
override
whenNotPaused
{
address owner = ownerOf(vars.profileId);
unchecked {
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
SET_PROFILE_IMAGE_URI_WITH_SIG_TYPEHASH,
vars.profileId,
keccak256(bytes(vars.imageURI)),
sigNonces[owner]++,
vars.sig.deadline
)
)
),
owner,
vars.sig
);
}
_setProfileImageURI(vars.profileId, vars.imageURI);
ProfileLib.setProfileImageURIWithSig(vars);
}
/// @inheritdoc ILensHub
@@ -319,8 +273,7 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
override
whenNotPaused
{
_validateCallerIsProfileOwnerOrDispatcher(profileId);
_setFollowNFTURI(profileId, followNFTURI);
ProfileLib.setFollowNFTURI(profileId, followNFTURI);
}
/// @inheritdoc ILensHub
@@ -329,25 +282,7 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
override
whenNotPaused
{
address owner = ownerOf(vars.profileId);
unchecked {
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
SET_FOLLOW_NFT_URI_WITH_SIG_TYPEHASH,
vars.profileId,
keccak256(bytes(vars.followNFTURI)),
sigNonces[owner]++,
vars.sig.deadline
)
)
),
owner,
vars.sig
);
}
_setFollowNFTURI(vars.profileId, vars.followNFTURI);
ProfileLib.setFollowNFTURIWithSig(vars);
}
/// @inheritdoc ILensHub
@@ -357,16 +292,7 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
whenPublishingEnabled
returns (uint256)
{
_validateCallerIsProfileOwnerOrDispatcher(vars.profileId);
return
_createPost(
vars.profileId,
vars.contentURI,
vars.collectModule,
vars.collectModuleInitData,
vars.referenceModule,
vars.referenceModuleInitData
);
return PublishingLib.post(vars);
}
/// @inheritdoc ILensHub
@@ -376,37 +302,7 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
whenPublishingEnabled
returns (uint256)
{
address owner = ownerOf(vars.profileId);
unchecked {
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
POST_WITH_SIG_TYPEHASH,
vars.profileId,
keccak256(bytes(vars.contentURI)),
vars.collectModule,
keccak256(vars.collectModuleInitData),
vars.referenceModule,
keccak256(vars.referenceModuleInitData),
sigNonces[owner]++,
vars.sig.deadline
)
)
),
owner,
vars.sig
);
}
return
_createPost(
vars.profileId,
vars.contentURI,
vars.collectModule,
vars.collectModuleInitData,
vars.referenceModule,
vars.referenceModuleInitData
);
return PublishingLib.postWithSig(vars);
}
/// @inheritdoc ILensHub
@@ -416,8 +312,7 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
whenPublishingEnabled
returns (uint256)
{
_validateCallerIsProfileOwnerOrDispatcher(vars.profileId);
return _createComment(vars);
return PublishingLib.comment(vars);
}
/// @inheritdoc ILensHub
@@ -427,45 +322,7 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
whenPublishingEnabled
returns (uint256)
{
address owner = ownerOf(vars.profileId);
unchecked {
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
COMMENT_WITH_SIG_TYPEHASH,
vars.profileId,
keccak256(bytes(vars.contentURI)),
vars.profileIdPointed,
vars.pubIdPointed,
keccak256(vars.referenceModuleData),
vars.collectModule,
keccak256(vars.collectModuleInitData),
vars.referenceModule,
keccak256(vars.referenceModuleInitData),
sigNonces[owner]++,
vars.sig.deadline
)
)
),
owner,
vars.sig
);
}
return
_createComment(
DataTypes.CommentData(
vars.profileId,
vars.contentURI,
vars.profileIdPointed,
vars.pubIdPointed,
vars.referenceModuleData,
vars.collectModule,
vars.collectModuleInitData,
vars.referenceModule,
vars.referenceModuleInitData
)
);
return PublishingLib.commentWithSig(vars);
}
/// @inheritdoc ILensHub
@@ -475,8 +332,7 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
whenPublishingEnabled
returns (uint256)
{
_validateCallerIsProfileOwnerOrDispatcher(vars.profileId);
return _createMirror(vars);
return PublishingLib.mirror(vars);
}
/// @inheritdoc ILensHub
@@ -486,67 +342,27 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
whenPublishingEnabled
returns (uint256)
{
address owner = ownerOf(vars.profileId);
unchecked {
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
MIRROR_WITH_SIG_TYPEHASH,
vars.profileId,
vars.profileIdPointed,
vars.pubIdPointed,
keccak256(vars.referenceModuleData),
vars.referenceModule,
keccak256(vars.referenceModuleInitData),
sigNonces[owner]++,
vars.sig.deadline
)
)
),
owner,
vars.sig
);
}
return
_createMirror(
DataTypes.MirrorData(
vars.profileId,
vars.profileIdPointed,
vars.pubIdPointed,
vars.referenceModuleData,
vars.referenceModule,
vars.referenceModuleInitData
)
);
return PublishingLib.mirrorWithSig(vars);
}
/**
* @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.
* @notice Burns a profile, this maintains the profile data struct.
*/
function burn(uint256 tokenId) public override whenNotPaused {
super.burn(tokenId);
_clearHandleHash(tokenId);
if (!_isApprovedOrOwner(msg.sender, tokenId)) revert Errors.NotOwnerOrApproved();
_burn(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.
* @notice Burns a profile with a signature, this maintains the profile data struct.
*/
function burnWithSig(uint256 tokenId, DataTypes.EIP712Signature calldata sig)
public
override
whenNotPaused
{
super.burnWithSig(tokenId, sig);
_clearHandleHash(tokenId);
GeneralLib.baseBurnWithSig(tokenId, sig);
_burn(tokenId);
}
/// ***************************************
@@ -554,20 +370,19 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
/// ***************************************
/// @inheritdoc ILensHub
function follow(uint256[] calldata profileIds, bytes[] calldata datas)
external
override
whenNotPaused
returns (uint256[] memory)
{
function follow(
uint256 followerProfileId,
uint256[] calldata idsOfProfilesToFollow,
uint256[] calldata followTokenIds,
bytes[] calldata datas
) external override whenNotPaused returns (uint256[] memory) {
return
InteractionLogic.follow(
msg.sender,
profileIds,
datas,
_profileById,
_profileIdByHandleHash
);
GeneralLib.follow({
followerProfileId: followerProfileId,
idsOfProfilesToFollow: idsOfProfilesToFollow,
followTokenIds: followTokenIds,
followModuleDatas: datas
});
}
/// @inheritdoc ILensHub
@@ -577,57 +392,64 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
whenNotPaused
returns (uint256[] memory)
{
uint256 dataLength = vars.datas.length;
bytes32[] memory dataHashes = new bytes32[](dataLength);
for (uint256 i = 0; i < dataLength; ) {
dataHashes[i] = keccak256(vars.datas[i]);
unchecked {
++i;
}
}
unchecked {
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
FOLLOW_WITH_SIG_TYPEHASH,
keccak256(abi.encodePacked(vars.profileIds)),
keccak256(abi.encodePacked(dataHashes)),
sigNonces[vars.follower]++,
vars.sig.deadline
)
)
),
vars.follower,
vars.sig
);
}
return GeneralLib.followWithSig(vars);
}
/// @inheritdoc ILensHub
function unfollow(uint256 unfollowerProfileId, uint256[] calldata idsOfProfilesToUnfollow)
external
override
whenNotPaused
{
return
InteractionLogic.follow(
vars.follower,
vars.profileIds,
vars.datas,
_profileById,
_profileIdByHandleHash
);
GeneralLib.unfollow({
unfollowerProfileId: unfollowerProfileId,
idsOfProfilesToUnfollow: idsOfProfilesToUnfollow
});
}
/// @inheritdoc ILensHub
function unfollowWithSig(DataTypes.UnfollowWithSigData calldata vars)
external
override
whenNotPaused
{
return GeneralLib.unfollowWithSig(vars);
}
/// @inheritdoc ILensHub
function setBlockStatus(
uint256 byProfileId,
uint256[] calldata idsOfProfilesToSetBlockStatus,
bool[] calldata blockStatus
) external override whenNotPaused {
return GeneralLib.setBlockStatus(byProfileId, idsOfProfilesToSetBlockStatus, blockStatus);
}
/// @inheritdoc ILensHub
function setBlockStatusWithSig(DataTypes.SetBlockStatusWithSigData calldata vars)
external
override
whenNotPaused
{
return GeneralLib.setBlockStatusWithSig(vars);
}
/// @inheritdoc ILensHub
function collect(
uint256 profileId,
uint256 collectorProfileId,
uint256 publisherProfileId, // TODO: Think if we can have better naming
uint256 pubId,
bytes calldata data
) external override whenNotPaused returns (uint256) {
return
InteractionLogic.collect(
msg.sender,
profileId,
pubId,
data,
COLLECT_NFT_IMPL,
_pubByIdByProfile,
_profileById
);
GeneralLib.collect({
collectorProfileId: collectorProfileId,
publisherProfileId: publisherProfileId,
pubId: pubId,
collectModuleData: data,
collectNFTImpl: COLLECT_NFT_IMPL
});
}
/// @inheritdoc ILensHub
@@ -637,34 +459,7 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
whenNotPaused
returns (uint256)
{
unchecked {
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
COLLECT_WITH_SIG_TYPEHASH,
vars.profileId,
vars.pubId,
keccak256(vars.data),
sigNonces[vars.collector]++,
vars.sig.deadline
)
)
),
vars.collector,
vars.sig
);
}
return
InteractionLogic.collect(
vars.collector,
vars.profileId,
vars.pubId,
vars.data,
COLLECT_NFT_IMPL,
_pubByIdByProfile,
_profileById
);
return GeneralLib.collectWithSig(vars, COLLECT_NFT_IMPL);
}
/// @inheritdoc ILensHub
@@ -699,10 +494,31 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
);
}
/// @inheritdoc ILensHub
function emitUnfollowedEvent(uint256 unfollowerProfileId, uint256 idOfProfileUnfollowed)
external
override
{
address expectedFollowNFT = _profileById[idOfProfileUnfollowed].followNFT;
if (msg.sender != expectedFollowNFT) {
revert Errors.CallerNotFollowNFT();
}
emit Events.Unfollowed(unfollowerProfileId, idOfProfileUnfollowed, block.timestamp);
}
/// *********************************
/// *****EXTERNAL VIEW FUNCTIONS*****
/// *********************************
function isFollowing(uint256 followerProfileId, uint256 followedProfileId)
external
view
returns (bool)
{
address followNFT = _profileById[followedProfileId].followNFT;
return followNFT != address(0) && IFollowNFT(followNFT).isFollowing(followerProfileId);
}
/// @inheritdoc ILensHub
function isProfileCreatorWhitelisted(address profileCreator)
external
@@ -713,11 +529,6 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
return _profileCreatorWhitelisted[profileCreator];
}
/// @inheritdoc ILensHub
function defaultProfile(address wallet) external view override returns (uint256) {
return _defaultProfileByAddress[wallet];
}
/// @inheritdoc ILensHub
function isFollowModuleWhitelisted(address followModule) external view override returns (bool) {
return _followModuleWhitelisted[followModule];
@@ -748,6 +559,35 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
return _governance;
}
/// @inheritdoc ILensHub
function isDelegatedExecutorApproved(address wallet, address executor)
external
view
returns (bool)
{
return _delegatedExecutorApproval[wallet][executor];
}
/// @inheritdoc ILensHub
function isBlocked(uint256 profileId, uint256 byProfileId) external view returns (bool) {
return _blockedStatus[byProfileId][profileId];
}
/// @inheritdoc ILensHub
function getDefaultProfile(address wallet) external view override returns (uint256) {
return _defaultProfileByAddress[wallet];
}
/// @inheritdoc ILensHub
function getProfileMetadataURI(uint256 profileId)
external
view
override
returns (string memory)
{
return _metadataByProfile[profileId];
}
/// @inheritdoc ILensHub
function getDispatcher(uint256 profileId) external view override returns (address) {
return _dispatcherByProfile[profileId];
@@ -758,6 +598,11 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
return _profileById[profileId].pubCount;
}
/// @inheritdoc ILensHub
function getProfileImageURI(uint256 profileId) external view override returns (string memory) {
return _profileById[profileId].imageURI;
}
/// @inheritdoc ILensHub
function getFollowNFT(uint256 profileId) external view override returns (address) {
return _profileById[profileId].followNFT;
@@ -803,11 +648,6 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
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
@@ -827,18 +667,7 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
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];
return GeneralLib.getContentURI(profileId, pubId);
}
/// @inheritdoc ILensHub
@@ -879,21 +708,6 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
}
}
/**
* @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) {
address followNFT = _profileById[tokenId].followNFT;
return
ProfileTokenURILogic.getProfileTokenURI(
tokenId,
followNFT == address(0) ? 0 : IERC721Enumerable(followNFT).totalSupply(),
ownerOf(tokenId),
_profileById[tokenId].handle,
_profileById[tokenId].imageURI
);
}
/// @inheritdoc ILensHub
function getFollowNFTImpl() external view override returns (address) {
return FOLLOW_NFT_IMPL;
@@ -904,81 +718,34 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
return COLLECT_NFT_IMPL;
}
/**
* @dev Overrides the LensNFTBase function to compute the domain separator in the GeneralLib.
*/
function getDomainSeparator() external view override returns (bytes32) {
return GeneralLib.getDomainSeparator();
}
/**
* @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) {
address followNFT = _profileById[tokenId].followNFT;
return
ProfileTokenURILogic.getProfileTokenURI(
tokenId,
followNFT == address(0) ? 0 : IERC721Enumerable(followNFT).totalSupply(),
ownerOf(tokenId),
'Lens Profile',
_profileById[tokenId].imageURI
);
}
/// ****************************
/// *****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 returns (uint256) {
unchecked {
uint256 pubId = ++_profileById[profileId].pubCount;
PublishingLogic.createPost(
profileId,
contentURI,
collectModule,
collectModuleData,
referenceModule,
referenceModuleData,
pubId,
_pubByIdByProfile,
_collectModuleWhitelisted,
_referenceModuleWhitelisted
);
return pubId;
}
}
/*
* If the profile ID is zero, this is the equivalent of "unsetting" a default profile.
* Note that the wallet address should either be the message sender or validated via a signature
* prior to this function call.
*/
function _setDefaultProfile(address wallet, uint256 profileId) internal {
if (profileId > 0 && wallet != ownerOf(profileId)) revert Errors.NotProfileOwner();
_defaultProfileByAddress[wallet] = profileId;
emit Events.DefaultProfileSet(wallet, profileId, block.timestamp);
}
function _createComment(DataTypes.CommentData memory vars) internal returns (uint256) {
unchecked {
uint256 pubId = ++_profileById[vars.profileId].pubCount;
PublishingLogic.createComment(
vars,
pubId,
_profileById,
_pubByIdByProfile,
_collectModuleWhitelisted,
_referenceModuleWhitelisted
);
return pubId;
}
}
function _createMirror(DataTypes.MirrorData memory vars) internal returns (uint256) {
unchecked {
uint256 pubId = ++_profileById[vars.profileId].pubCount;
PublishingLogic.createMirror(
vars,
pubId,
_pubByIdByProfile,
_referenceModuleWhitelisted
);
return pubId;
}
GeneralLib.setGovernance(newGovernance);
}
function _setDispatcher(uint256 profileId, address dispatcher) internal {
@@ -986,23 +753,6 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
emit Events.DispatcherSet(profileId, dispatcher, block.timestamp);
}
function _setProfileImageURI(uint256 profileId, string calldata imageURI) internal {
if (bytes(imageURI).length > Constants.MAX_PROFILE_IMAGE_URI_LENGTH)
revert Errors.ProfileImageURILengthInvalid();
_profileById[profileId].imageURI = imageURI;
emit Events.ProfileImageURISet(profileId, imageURI, block.timestamp);
}
function _setFollowNFTURI(uint256 profileId, string calldata 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,
@@ -1019,13 +769,6 @@ contract LensHub is LensNFTBase, VersionedInitializable, LensMultiState, LensHub
super._beforeTokenTransfer(from, to, tokenId);
}
function _validateCallerIsProfileOwnerOrDispatcher(uint256 profileId) internal view {
if (msg.sender == ownerOf(profileId) || msg.sender == _dispatcherByProfile[profileId]) {
return;
}
revert Errors.NotProfileOwnerOrDispatcher();
}
function _validateCallerIsProfileOwner(uint256 profileId) internal view {
if (msg.sender != ownerOf(profileId)) revert Errors.NotProfileOwner();
}

View File

@@ -0,0 +1,85 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Errors} from '../../libraries/Errors.sol';
import {IERC165} from '@openzeppelin/contracts/utils/introspection/IERC165.sol';
import {IERC2981} from '@openzeppelin/contracts/interfaces/IERC2981.sol';
abstract contract ERC2981CollectionRoyalties is IERC2981 {
uint16 internal constant BASIS_POINTS = 10000;
// bytes4(keccak256('royaltyInfo(uint256,uint256)')) == 0x2a55205a
bytes4 internal constant INTERFACE_ID_ERC2981 = 0x2a55205a;
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
return interfaceId == INTERFACE_ID_ERC2981 || interfaceId == type(IERC165).interfaceId;
}
/**
* @notice Changes the royalty percentage for secondary sales.
*
* @param royaltiesInBasisPoints The royalty percentage meassured in basis points.
*/
function setRoyalty(uint256 royaltiesInBasisPoints) external {
_beforeRoyaltiesSet(royaltiesInBasisPoints);
_setRoyalty(royaltiesInBasisPoints);
}
/**
* @notice Called with the sale price to determine how much royalty is owed and to whom.
*
* @param tokenId The ID of the token queried for royalty information.
* @param salePrice The sale price of the token specified.
* @return A tuple with the address who should receive the royalties and the royalty
* payment amount for the given sale price.
*/
function royaltyInfo(uint256 tokenId, uint256 salePrice)
external
view
returns (address, uint256)
{
return (_getReceiver(tokenId), _getRoyaltyAmount(tokenId, salePrice));
}
function _setRoyalty(uint256 royaltiesInBasisPoints) internal virtual {
if (royaltiesInBasisPoints > BASIS_POINTS) {
revert Errors.InvalidParameter();
} else {
_storeRoyaltiesInBasisPoints(royaltiesInBasisPoints);
}
}
function _getRoyaltyAmount(uint256 tokenId, uint256 salePrice)
internal
view
virtual
returns (uint256)
{
return (salePrice * _loadRoyaltiesInBasisPoints()) / BASIS_POINTS;
}
function _storeRoyaltiesInBasisPoints(uint256 royaltiesInBasisPoints) internal virtual {
uint256 royaltiesInBasisPointsSlot = _getRoyaltiesInBasisPointsSlot();
assembly {
sstore(royaltiesInBasisPointsSlot, royaltiesInBasisPoints)
}
}
function _loadRoyaltiesInBasisPoints() internal view virtual returns (uint256) {
uint256 royaltiesInBasisPointsSlot = _getRoyaltiesInBasisPointsSlot();
uint256 royaltyAmount;
assembly {
royaltyAmount := sload(royaltiesInBasisPointsSlot)
}
return royaltyAmount;
}
function _beforeRoyaltiesSet(uint256 royaltiesInBasisPoints) internal view virtual {}
function _getRoyaltiesInBasisPointsSlot() internal view virtual returns (uint256);
function _getReceiver(uint256 tokenId) internal view virtual returns (address);
}

View File

@@ -2,8 +2,10 @@
pragma solidity ^0.8.0;
import './ERC721Time.sol';
import '@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol';
import {Errors} from '../../libraries/Errors.sol';
import {ERC721Time} from './ERC721Time.sol';
import {IERC721Enumerable} from '@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol';
import {IERC165} from '@openzeppelin/contracts/interfaces/IERC165.sol';
/**
* @dev This implements an optional extension of {ERC721} defined in the EIP that adds
@@ -50,12 +52,15 @@ abstract contract ERC721Enumerable is ERC721Time, IERC721Enumerable {
override
returns (uint256)
{
require(index < ERC721Time.balanceOf(owner), 'ERC721Enumerable: owner index out of bounds');
if (index >= ERC721Time.balanceOf(owner))
revert Errors.ERC721Enumerable_OwnerIndexOutOfBounds();
return _ownedTokens[owner][index];
}
/**
* @dev See {IERC721Enumerable-totalSupply}.
* @dev TotalSupply is decreased when the Profile is burned.
* @dev If you're looking how to get the next ProfileId created - see _profileCounter
*/
function totalSupply() public view virtual override returns (uint256) {
return _allTokens.length;
@@ -65,10 +70,8 @@ abstract contract ERC721Enumerable is ERC721Time, IERC721Enumerable {
* @dev See {IERC721Enumerable-tokenByIndex}.
*/
function tokenByIndex(uint256 index) public view virtual override returns (uint256) {
require(
index < ERC721Enumerable.totalSupply(),
'ERC721Enumerable: global index out of bounds'
);
if (index >= ERC721Enumerable.totalSupply())
revert Errors.ERC721Enumerable_GlobalIndexOutOfBounds();
return _allTokens[index];
}

View File

@@ -2,13 +2,16 @@
pragma solidity ^0.8.0;
import './IERC721Time.sol';
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 {Errors} from '../../libraries/Errors.sol';
import {IERC721Time} from '../../interfaces/IERC721Time.sol';
import {IERC721Receiver} from '@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol';
import {IERC721Metadata} from '@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol';
import {Address} from '@openzeppelin/contracts/utils/Address.sol';
import {Context} from '@openzeppelin/contracts/utils/Context.sol';
import {Strings} from '@openzeppelin/contracts/utils/Strings.sol';
import {ERC165} from '@openzeppelin/contracts/utils/introspection/ERC165.sol';
import {IERC165} from '@openzeppelin/contracts/interfaces/IERC165.sol';
import {IERC721} from '@openzeppelin/contracts/interfaces/IERC721.sol';
/**
* @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including
@@ -74,7 +77,7 @@ abstract contract ERC721Time is Context, ERC165, IERC721Time, IERC721Metadata {
* @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');
if (owner == address(0)) revert Errors.ERC721Time_BalanceQueryForZeroAddress();
return _balances[owner];
}
@@ -83,7 +86,7 @@ abstract contract ERC721Time is Context, ERC165, IERC721Time, IERC721Metadata {
*/
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');
if (owner == address(0)) revert Errors.ERC721Time_OwnerQueryForNonexistantToken();
return owner;
}
@@ -92,12 +95,12 @@ abstract contract ERC721Time is Context, ERC165, IERC721Time, IERC721Metadata {
*/
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');
if (mintTimestamp == 0) revert Errors.ERC721Time_MintTimestampQueryForNonexistantToken();
return mintTimestamp;
}
/**
* @dev See {IERC721Time-mintTimestampOf}
* @dev See {IERC721Time-tokenDataOf}
*/
function tokenDataOf(uint256 tokenId)
public
@@ -106,7 +109,7 @@ abstract contract ERC721Time is Context, ERC165, IERC721Time, IERC721Metadata {
override
returns (IERC721Time.TokenData memory)
{
require(_exists(tokenId), 'ERC721: token data query for nonexistent token');
if (!_exists(tokenId)) revert Errors.ERC721Time_TokenDataQueryForNonexistantToken();
return _tokenData[tokenId];
}
@@ -135,7 +138,7 @@ abstract contract ERC721Time is Context, ERC165, IERC721Time, IERC721Metadata {
* @dev See {IERC721Metadata-tokenURI}.
*/
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
require(_exists(tokenId), 'ERC721Metadata: URI query for nonexistent token');
if (!_exists(tokenId)) revert Errors.ERC721Time_URIQueryForNonexistantToken();
string memory baseURI = _baseURI();
return
@@ -156,12 +159,10 @@ abstract contract ERC721Time is Context, ERC165, IERC721Time, IERC721Metadata {
*/
function approve(address to, uint256 tokenId) public virtual override {
address owner = ERC721Time.ownerOf(tokenId);
require(to != owner, 'ERC721: approval to current owner');
if (to == owner) revert Errors.ERC721Time_ApprovalToCurrentOwner();
require(
_msgSender() == owner || isApprovedForAll(owner, _msgSender()),
'ERC721: approve caller is not owner nor approved for all'
);
if (_msgSender() != owner && !isApprovedForAll(owner, _msgSender()))
revert Errors.ERC721Time_ApproveCallerNotOwnerOrApprovedForAll();
_approve(to, tokenId);
}
@@ -170,7 +171,7 @@ abstract contract ERC721Time is Context, ERC165, IERC721Time, IERC721Metadata {
* @dev See {IERC721-getApproved}.
*/
function getApproved(uint256 tokenId) public view virtual override returns (address) {
require(_exists(tokenId), 'ERC721: approved query for nonexistent token');
if (!_exists(tokenId)) revert Errors.ERC721Time_ApprovedQueryForNonexistantToken();
return _tokenApprovals[tokenId];
}
@@ -179,7 +180,7 @@ abstract contract ERC721Time is Context, ERC165, IERC721Time, IERC721Metadata {
* @dev See {IERC721-setApprovalForAll}.
*/
function setApprovalForAll(address operator, bool approved) public virtual override {
require(operator != _msgSender(), 'ERC721: approve to caller');
if (operator == _msgSender()) revert Errors.ERC721Time_ApproveToCaller();
_setOperatorApproval(_msgSender(), operator, approved);
}
@@ -206,10 +207,8 @@ abstract contract ERC721Time is Context, ERC165, IERC721Time, IERC721Metadata {
uint256 tokenId
) public virtual override {
//solhint-disable-next-line max-line-length
require(
_isApprovedOrOwner(_msgSender(), tokenId),
'ERC721: transfer caller is not owner nor approved'
);
if (!_isApprovedOrOwner(_msgSender(), tokenId))
revert Errors.ERC721Time_TransferCallerNotOwnerOrApproved();
_transfer(from, to, tokenId);
}
@@ -234,13 +233,24 @@ abstract contract ERC721Time is Context, ERC165, IERC721Time, IERC721Metadata {
uint256 tokenId,
bytes memory _data
) public virtual override {
require(
_isApprovedOrOwner(_msgSender(), tokenId),
'ERC721: transfer caller is not owner nor approved'
);
if (!_isApprovedOrOwner(_msgSender(), tokenId))
revert Errors.ERC721Time_TransferCallerNotOwnerOrApproved();
_safeTransfer(from, to, tokenId, _data);
}
/**
* @notice Returns the owner of the `tokenId` token.
*
* @dev It is prefixed as `unsafe` as it does not revert when the token does not exist.
*
* @param tokenId The token which owner is being queried.
*
* @return address The address owning the given token, zero address if the token does not exist.
*/
function _unsafeOwnerOf(uint256 tokenId) internal view returns (address) {
return _tokenData[tokenId].owner;
}
/**
* @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.
@@ -266,10 +276,8 @@ abstract contract ERC721Time is Context, ERC165, IERC721Time, IERC721Metadata {
bytes memory _data
) internal virtual {
_transfer(from, to, tokenId);
require(
_checkOnERC721Received(from, to, tokenId, _data),
'ERC721: transfer to non ERC721Receiver implementer'
);
if (!_checkOnERC721Received(from, to, tokenId, _data))
revert Errors.ERC721Time_TransferToNonERC721ReceiverImplementer();
}
/**
@@ -297,7 +305,7 @@ abstract contract ERC721Time is Context, ERC165, IERC721Time, IERC721Metadata {
virtual
returns (bool)
{
require(_exists(tokenId), 'ERC721: operator query for nonexistent token');
if (!_exists(tokenId)) revert Errors.ERC721Time_OperatorQueryForNonexistantToken();
address owner = ERC721Time.ownerOf(tokenId);
return (spender == owner ||
getApproved(tokenId) == spender ||
@@ -328,10 +336,8 @@ abstract contract ERC721Time is Context, ERC165, IERC721Time, IERC721Metadata {
bytes memory _data
) internal virtual {
_mint(to, tokenId);
require(
_checkOnERC721Received(address(0), to, tokenId, _data),
'ERC721: transfer to non ERC721Receiver implementer'
);
if (!_checkOnERC721Received(address(0), to, tokenId, _data))
revert Errors.ERC721Time_TransferToNonERC721ReceiverImplementer();
}
/**
@@ -347,12 +353,14 @@ abstract contract ERC721Time is Context, ERC165, IERC721Time, IERC721Metadata {
* 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');
if (to == address(0)) revert Errors.ERC721Time_MintToZeroAddress();
if (_exists(tokenId)) revert Errors.ERC721Time_TokenAlreadyMinted();
_beforeTokenTransfer(address(0), to, tokenId);
_balances[to] += 1;
unchecked {
++_balances[to];
}
_tokenData[tokenId].owner = to;
_tokenData[tokenId].mintTimestamp = uint96(block.timestamp);
@@ -377,7 +385,9 @@ abstract contract ERC721Time is Context, ERC165, IERC721Time, IERC721Metadata {
// Clear approvals
_approve(address(0), tokenId);
_balances[owner] -= 1;
unchecked {
--_balances[owner];
}
delete _tokenData[tokenId];
emit Transfer(owner, address(0), tokenId);
@@ -399,16 +409,19 @@ abstract contract ERC721Time is Context, ERC165, IERC721Time, IERC721Metadata {
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');
if (ERC721Time.ownerOf(tokenId) != from)
revert Errors.ERC721Time_TransferOfTokenThatIsNotOwn();
if (to == address(0)) revert Errors.ERC721Time_TransferToZeroAddress();
_beforeTokenTransfer(from, to, tokenId);
// Clear approvals from the previous owner
_approve(address(0), tokenId);
_balances[from] -= 1;
_balances[to] += 1;
unchecked {
--_balances[from];
++_balances[to];
}
_tokenData[tokenId].owner = to;
emit Transfer(from, to, tokenId);
@@ -462,7 +475,7 @@ abstract contract ERC721Time is Context, ERC165, IERC721Time, IERC721Metadata {
return retval == IERC721Receiver.onERC721Received.selector;
} catch (bytes memory reason) {
if (reason.length == 0) {
revert('ERC721: transfer to non ERC721Receiver implementer');
revert Errors.ERC721Time_TransferToNonERC721ReceiverImplementer();
} else {
assembly {
revert(add(32, reason), mload(reason))

View File

@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {Errors} from '../../libraries/Errors.sol';
import {Events} from '../../libraries/Events.sol';
/**
* @title HubRestricted
* @author Lens Protocol
*
* @notice This abstract contract adds a public `HUB` immutable field, validations when setting it, as well
* as an `onlyHub` modifier, to inherit from contracts that have functions restricted to be only called by the Lens hub.
*/
abstract contract HubRestricted {
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;
}
}

View File

@@ -1,21 +1,23 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {Events} from '../../libraries/Events.sol';
import {DataTypes} from '../../libraries/DataTypes.sol';
import {Errors} from '../../libraries/Errors.sol';
import {ILensMultiState} from '../../interfaces/ILensMultiState.sol';
/**
* @title LensMultiState
*
* @notice This is an abstract contract that implements internal LensHub state setting and validation.
* @notice This is an abstract contract that implements internal LensHub state validation. Setting
* is delegated to the GeneralLib.
*
* whenNotPaused: Either publishingPaused or Unpaused.
* whenPublishingEnabled: When Unpaused only.
*/
abstract contract LensMultiState {
DataTypes.ProtocolState private _state;
abstract contract LensMultiState is ILensMultiState {
DataTypes.ProtocolState private _state; // slot 12
modifier whenNotPaused() {
_validateNotPaused();
@@ -35,16 +37,10 @@ abstract contract LensMultiState {
* 1: PublishingPaused
* 2: Paused
*/
function getState() external view returns (DataTypes.ProtocolState) {
function getState() external view override 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.Unpaused) {
revert Errors.PublishingPaused();

View File

@@ -1,11 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
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 {MetaTxHelpers} from '../../libraries/helpers/MetaTxHelpers.sol';
import {ERC721Time} from './ERC721Time.sol';
import {ERC721Enumerable} from './ERC721Enumerable.sol';
@@ -55,11 +56,11 @@ abstract contract LensNFTBase is ERC721Enumerable, ILensNFTBase {
address spender,
uint256 tokenId,
DataTypes.EIP712Signature calldata sig
) external override {
) external virtual override {
if (spender == address(0)) revert Errors.ZeroSpender();
address owner = ownerOf(tokenId);
unchecked {
_validateRecoveredAddress(
MetaTxHelpers._validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
@@ -84,10 +85,10 @@ abstract contract LensNFTBase is ERC721Enumerable, ILensNFTBase {
address operator,
bool approved,
DataTypes.EIP712Signature calldata sig
) external override {
) external virtual override {
if (operator == address(0)) revert Errors.ZeroSpender();
unchecked {
_validateRecoveredAddress(
MetaTxHelpers._validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
@@ -108,7 +109,7 @@ abstract contract LensNFTBase is ERC721Enumerable, ILensNFTBase {
}
/// @inheritdoc ILensNFTBase
function getDomainSeparator() external view override returns (bytes32) {
function getDomainSeparator() external view virtual override returns (bytes32) {
return _calculateDomainSeparator();
}
@@ -126,7 +127,7 @@ abstract contract LensNFTBase is ERC721Enumerable, ILensNFTBase {
{
address owner = ownerOf(tokenId);
unchecked {
_validateRecoveredAddress(
MetaTxHelpers._validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
@@ -144,20 +145,6 @@ abstract contract LensNFTBase is ERC721Enumerable, ILensNFTBase {
_burn(tokenId);
}
/**
* @dev Wrapper for ecrecover to reduce code size, used in meta-tx specific functions.
*/
function _validateRecoveredAddress(
bytes32 digest,
address expectedAddress,
DataTypes.EIP712Signature calldata 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.
*/

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {Errors} from '../../libraries/Errors.sol';
import {Events} from '../../libraries/Events.sol';

View File

@@ -1,8 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {IFollowModule} from '../../interfaces/IFollowModule.sol';
import {IFollowModuleLegacy} from '../../interfaces/IFollowModuleLegacy.sol';
import {ILensHub} from '../../interfaces/ILensHub.sol';
import {Errors} from '../../libraries/Errors.sol';
import {Events} from '../../libraries/Events.sol';
@@ -33,7 +33,7 @@ abstract contract FollowValidationModuleBase is ModuleBase {
address followModule = ILensHub(HUB).getFollowModule(profileId);
bool isFollowing;
if (followModule != address(0)) {
isFollowing = IFollowModule(followModule).isFollowing(profileId, user, 0);
isFollowing = IFollowModuleLegacy(followModule).isFollowing(0, profileId, user, 0);
} else {
address followNFT = ILensHub(HUB).getFollowNFT(profileId);
isFollowing = followNFT != address(0) && IERC721(followNFT).balanceOf(user) != 0;

View File

@@ -1,28 +1,20 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {Errors} from '../../libraries/Errors.sol';
import {Events} from '../../libraries/Events.sol';
import {HubRestricted} from '../base/HubRestricted.sol';
/**
* @title ModuleBase
* @author Lens Protocol
*
* @notice This abstract contract adds a public `HUB` immutable to inheriting modules, as well as an
* `onlyHub` modifier.
* @notice This contract fires an event at construction, to be inherited by other modules, in addition to the
* HubRestricted contract features.
*/
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;
abstract contract ModuleBase is HubRestricted {
constructor(address hub) HubRestricted(hub) {
emit Events.ModuleBaseConstructed(hub, block.timestamp);
}
}

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {Errors} from '../../libraries/Errors.sol';
import {Events} from '../../libraries/Events.sol';

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {ICollectModule} from '../../../interfaces/ICollectModule.sol';
import {Errors} from '../../../libraries/Errors.sol';
@@ -61,6 +61,7 @@ contract FeeCollectModule is FeeModuleBase, FollowValidationModuleBase, ICollect
*/
function initializePublicationCollectModule(
uint256 profileId,
address,
uint256 pubId,
bytes calldata data
) external override onlyHub returns (bytes memory) {
@@ -94,7 +95,9 @@ contract FeeCollectModule is FeeModuleBase, FollowValidationModuleBase, ICollect
*/
function processCollect(
uint256 referrerProfileId,
uint256,
address collector,
address executor,
uint256 profileId,
uint256 pubId,
bytes calldata data
@@ -102,9 +105,9 @@ contract FeeCollectModule is FeeModuleBase, FollowValidationModuleBase, ICollect
if (_dataByPublicationByProfile[profileId][pubId].followerOnly)
_checkFollowValidity(profileId, collector);
if (referrerProfileId == profileId) {
_processCollect(collector, profileId, pubId, data);
_processCollect(executor, profileId, pubId, data);
} else {
_processCollectWithReferral(referrerProfileId, collector, profileId, pubId, data);
_processCollectWithReferral(referrerProfileId, executor, profileId, pubId, data);
}
}
@@ -126,7 +129,7 @@ contract FeeCollectModule is FeeModuleBase, FollowValidationModuleBase, ICollect
}
function _processCollect(
address collector,
address executor,
uint256 profileId,
uint256 pubId,
bytes calldata data
@@ -140,14 +143,14 @@ contract FeeCollectModule is FeeModuleBase, FollowValidationModuleBase, ICollect
uint256 treasuryAmount = (amount * treasuryFee) / BPS_MAX;
uint256 adjustedAmount = amount - treasuryAmount;
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
IERC20(currency).safeTransferFrom(executor, recipient, adjustedAmount);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
IERC20(currency).safeTransferFrom(executor, treasury, treasuryAmount);
}
function _processCollectWithReferral(
uint256 referrerProfileId,
address collector,
address executor,
uint256 profileId,
uint256 pubId,
bytes calldata data
@@ -177,12 +180,12 @@ contract FeeCollectModule is FeeModuleBase, FollowValidationModuleBase, ICollect
address referralRecipient = IERC721(HUB).ownerOf(referrerProfileId);
IERC20(currency).safeTransferFrom(collector, referralRecipient, referralAmount);
IERC20(currency).safeTransferFrom(executor, referralRecipient, referralAmount);
}
address recipient = _dataByPublicationByProfile[profileId][pubId].recipient;
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
IERC20(currency).safeTransferFrom(executor, recipient, adjustedAmount);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
IERC20(currency).safeTransferFrom(executor, treasury, treasuryAmount);
}
}

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {ICollectModule} from '../../../interfaces/ICollectModule.sol';
import {ModuleBase} from '../ModuleBase.sol';
@@ -24,6 +24,7 @@ contract FreeCollectModule is FollowValidationModuleBase, ICollectModule {
*/
function initializePublicationCollectModule(
uint256 profileId,
address,
uint256 pubId,
bytes calldata data
) external override onlyHub returns (bytes memory) {
@@ -37,11 +38,13 @@ contract FreeCollectModule is FollowValidationModuleBase, ICollectModule {
* 1. Ensuring the collector is a follower, if needed
*/
function processCollect(
uint256 referrerProfileId,
uint256,
uint256,
address collector,
address,
uint256 profileId,
uint256 pubId,
bytes calldata data
bytes calldata
) external view override {
if (_followerOnlyByPublicationByProfile[profileId][pubId])
_checkFollowValidity(profileId, collector);

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {ICollectModule} from '../../../interfaces/ICollectModule.sol';
import {Errors} from '../../../libraries/Errors.sol';
@@ -66,6 +66,7 @@ contract LimitedFeeCollectModule is FeeModuleBase, FollowValidationModuleBase, I
*/
function initializePublicationCollectModule(
uint256 profileId,
address,
uint256 pubId,
bytes calldata data
) external override onlyHub returns (bytes memory) {
@@ -103,7 +104,9 @@ contract LimitedFeeCollectModule is FeeModuleBase, FollowValidationModuleBase, I
*/
function processCollect(
uint256 referrerProfileId,
uint256,
address collector,
address executor,
uint256 profileId,
uint256 pubId,
bytes calldata data
@@ -111,16 +114,18 @@ contract LimitedFeeCollectModule is FeeModuleBase, FollowValidationModuleBase, I
if (_dataByPublicationByProfile[profileId][pubId].followerOnly)
_checkFollowValidity(profileId, collector);
if (
_dataByPublicationByProfile[profileId][pubId].currentCollects >=
_dataByPublicationByProfile[profileId][pubId].currentCollects ==
_dataByPublicationByProfile[profileId][pubId].collectLimit
) {
revert Errors.MintLimitExceeded();
} else {
++_dataByPublicationByProfile[profileId][pubId].currentCollects;
unchecked {
++_dataByPublicationByProfile[profileId][pubId].currentCollects;
}
if (referrerProfileId == profileId) {
_processCollect(collector, profileId, pubId, data);
_processCollect(executor, profileId, pubId, data);
} else {
_processCollectWithReferral(referrerProfileId, collector, profileId, pubId, data);
_processCollectWithReferral(referrerProfileId, executor, profileId, pubId, data);
}
}
}
@@ -143,7 +148,7 @@ contract LimitedFeeCollectModule is FeeModuleBase, FollowValidationModuleBase, I
}
function _processCollect(
address collector,
address executor,
uint256 profileId,
uint256 pubId,
bytes calldata data
@@ -157,14 +162,14 @@ contract LimitedFeeCollectModule is FeeModuleBase, FollowValidationModuleBase, I
uint256 treasuryAmount = (amount * treasuryFee) / BPS_MAX;
uint256 adjustedAmount = amount - treasuryAmount;
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
IERC20(currency).safeTransferFrom(executor, recipient, adjustedAmount);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
IERC20(currency).safeTransferFrom(executor, treasury, treasuryAmount);
}
function _processCollectWithReferral(
uint256 referrerProfileId,
address collector,
address executor,
uint256 profileId,
uint256 pubId,
bytes calldata data
@@ -194,12 +199,12 @@ contract LimitedFeeCollectModule is FeeModuleBase, FollowValidationModuleBase, I
address referralRecipient = IERC721(HUB).ownerOf(referrerProfileId);
IERC20(currency).safeTransferFrom(collector, referralRecipient, referralAmount);
IERC20(currency).safeTransferFrom(executor, referralRecipient, referralAmount);
}
address recipient = _dataByPublicationByProfile[profileId][pubId].recipient;
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
IERC20(currency).safeTransferFrom(executor, recipient, adjustedAmount);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
IERC20(currency).safeTransferFrom(executor, treasury, treasuryAmount);
}
}

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {ICollectModule} from '../../../interfaces/ICollectModule.sol';
import {Errors} from '../../../libraries/Errors.sol';
@@ -71,6 +71,7 @@ contract LimitedTimedFeeCollectModule is FeeModuleBase, FollowValidationModuleBa
*/
function initializePublicationCollectModule(
uint256 profileId,
address,
uint256 pubId,
bytes calldata data
) external override onlyHub returns (bytes memory) {
@@ -123,7 +124,9 @@ contract LimitedTimedFeeCollectModule is FeeModuleBase, FollowValidationModuleBa
*/
function processCollect(
uint256 referrerProfileId,
uint256,
address collector,
address executor,
uint256 profileId,
uint256 pubId,
bytes calldata data
@@ -141,9 +144,9 @@ contract LimitedTimedFeeCollectModule is FeeModuleBase, FollowValidationModuleBa
} else {
++_dataByPublicationByProfile[profileId][pubId].currentCollects;
if (referrerProfileId == profileId) {
_processCollect(collector, profileId, pubId, data);
_processCollect(executor, profileId, pubId, data);
} else {
_processCollectWithReferral(referrerProfileId, collector, profileId, pubId, data);
_processCollectWithReferral(referrerProfileId, executor, profileId, pubId, data);
}
}
}
@@ -166,7 +169,7 @@ contract LimitedTimedFeeCollectModule is FeeModuleBase, FollowValidationModuleBa
}
function _processCollect(
address collector,
address executor,
uint256 profileId,
uint256 pubId,
bytes calldata data
@@ -180,14 +183,14 @@ contract LimitedTimedFeeCollectModule is FeeModuleBase, FollowValidationModuleBa
uint256 treasuryAmount = (amount * treasuryFee) / BPS_MAX;
uint256 adjustedAmount = amount - treasuryAmount;
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
IERC20(currency).safeTransferFrom(executor, recipient, adjustedAmount);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
IERC20(currency).safeTransferFrom(executor, treasury, treasuryAmount);
}
function _processCollectWithReferral(
uint256 referrerProfileId,
address collector,
address executor,
uint256 profileId,
uint256 pubId,
bytes calldata data
@@ -217,12 +220,12 @@ contract LimitedTimedFeeCollectModule is FeeModuleBase, FollowValidationModuleBa
address referralRecipient = IERC721(HUB).ownerOf(referrerProfileId);
IERC20(currency).safeTransferFrom(collector, referralRecipient, referralAmount);
IERC20(currency).safeTransferFrom(executor, referralRecipient, referralAmount);
}
address recipient = _dataByPublicationByProfile[profileId][pubId].recipient;
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
IERC20(currency).safeTransferFrom(executor, recipient, adjustedAmount);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
IERC20(currency).safeTransferFrom(executor, treasury, treasuryAmount);
}
}

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {ICollectModule} from '../../../interfaces/ICollectModule.sol';
import {Errors} from '../../../libraries/Errors.sol';
@@ -18,9 +18,10 @@ contract RevertCollectModule is ICollectModule {
* @dev There is nothing needed at initialization.
*/
function initializePublicationCollectModule(
uint256 profileId,
uint256 pubId,
bytes calldata data
uint256,
address,
uint256,
bytes calldata
) external pure override returns (bytes memory) {
return new bytes(0);
}
@@ -30,11 +31,13 @@ contract RevertCollectModule is ICollectModule {
* 1. Always reverting
*/
function processCollect(
uint256 referrerProfileId,
address collector,
uint256 profileId,
uint256 pubId,
bytes calldata data
uint256,
uint256,
address,
address,
uint256,
uint256,
bytes calldata
) external pure override {
revert Errors.CollectNotAllowed();
}

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {ICollectModule} from '../../../interfaces/ICollectModule.sol';
import {IFollowModule} from '../../../interfaces/IFollowModule.sol';
@@ -70,6 +70,7 @@ contract TimedFeeCollectModule is FeeModuleBase, FollowValidationModuleBase, ICo
*/
function initializePublicationCollectModule(
uint256 profileId,
address,
uint256 pubId,
bytes calldata data
) external override onlyHub returns (bytes memory) {
@@ -109,7 +110,9 @@ contract TimedFeeCollectModule is FeeModuleBase, FollowValidationModuleBase, ICo
*/
function processCollect(
uint256 referrerProfileId,
uint256,
address collector,
address executor,
uint256 profileId,
uint256 pubId,
bytes calldata data
@@ -120,9 +123,9 @@ contract TimedFeeCollectModule is FeeModuleBase, FollowValidationModuleBase, ICo
if (block.timestamp > endTimestamp) revert Errors.CollectExpired();
if (referrerProfileId == profileId) {
_processCollect(collector, profileId, pubId, data);
_processCollect(executor, profileId, pubId, data);
} else {
_processCollectWithReferral(referrerProfileId, collector, profileId, pubId, data);
_processCollectWithReferral(referrerProfileId, executor, profileId, pubId, data);
}
}
@@ -144,7 +147,7 @@ contract TimedFeeCollectModule is FeeModuleBase, FollowValidationModuleBase, ICo
}
function _processCollect(
address collector,
address executor,
uint256 profileId,
uint256 pubId,
bytes calldata data
@@ -158,14 +161,14 @@ contract TimedFeeCollectModule is FeeModuleBase, FollowValidationModuleBase, ICo
uint256 treasuryAmount = (amount * treasuryFee) / BPS_MAX;
uint256 adjustedAmount = amount - treasuryAmount;
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
IERC20(currency).safeTransferFrom(executor, recipient, adjustedAmount);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
IERC20(currency).safeTransferFrom(executor, treasury, treasuryAmount);
}
function _processCollectWithReferral(
uint256 referrerProfileId,
address collector,
address executor,
uint256 profileId,
uint256 pubId,
bytes calldata data
@@ -195,12 +198,12 @@ contract TimedFeeCollectModule is FeeModuleBase, FollowValidationModuleBase, ICo
address referralRecipient = IERC721(HUB).ownerOf(referrerProfileId);
IERC20(currency).safeTransferFrom(collector, referralRecipient, referralAmount);
IERC20(currency).safeTransferFrom(executor, referralRecipient, referralAmount);
}
address recipient = _dataByPublicationByProfile[profileId][pubId].recipient;
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
IERC20(currency).safeTransferFrom(executor, recipient, adjustedAmount);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
IERC20(currency).safeTransferFrom(executor, treasury, treasuryAmount);
}
}

View File

@@ -0,0 +1,188 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {IDeprecatedCollectModule} from '../../../../interfaces/IDeprecatedCollectModule.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 followerOnly Whether only followers should be able to collect.
*/
struct ProfilePublicationData {
uint256 amount;
address currency;
address recipient;
uint16 referralFee;
bool followerOnly;
}
/**
* @title FeeCollectModule
* @author Lens Protocol
*
* @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 DeprecatedFeeCollectModule is FeeModuleBase, FollowValidationModuleBase, IDeprecatedCollectModule {
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.
* bool followerOnly: Whether only followers should be able to collect.
*
* @return bytes 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,
bool followerOnly
) = abi.decode(data, (uint256, address, address, uint16, bool));
if (
!_currencyWhitelisted(currency) ||
recipient == address(0) ||
referralFee > BPS_MAX ||
amount == 0
) 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].followerOnly = followerOnly;
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 {
if (_dataByPublicationByProfile[profileId][pubId].followerOnly)
_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 ProfilePublicationData 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);
if (treasuryAmount > 0)
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);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
}
}

View File

@@ -0,0 +1,49 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {IDeprecatedCollectModule} from '../../../../interfaces/IDeprecatedCollectModule.sol';
import {ModuleBase} from '../../ModuleBase.sol';
import {FollowValidationModuleBase} from '../../FollowValidationModuleBase.sol';
/**
* @title FreeCollectModule
* @author Lens Protocol
*
* @notice This is a simple Lens CollectModule implementation, inheriting from the ICollectModule interface.
*
* This module works by allowing all collects.
*/
contract DeprecatedFreeCollectModule is FollowValidationModuleBase, IDeprecatedCollectModule {
constructor(address hub) ModuleBase(hub) {}
mapping(uint256 => mapping(uint256 => bool)) internal _followerOnlyByPublicationByProfile;
/**
* @dev There is nothing needed at initialization.
*/
function initializePublicationCollectModule(
uint256 profileId,
uint256 pubId,
bytes calldata data
) external override onlyHub returns (bytes memory) {
bool followerOnly = abi.decode(data, (bool));
if (followerOnly) _followerOnlyByPublicationByProfile[profileId][pubId] = true;
return data;
}
/**
* @dev Processes a collect by:
* 1. Ensuring the collector is a follower, if needed
*/
function processCollect(
uint256,
address collector,
uint256 profileId,
uint256 pubId,
bytes calldata
) external view override {
if (_followerOnlyByPublicationByProfile[profileId][pubId])
_checkFollowValidity(profileId, collector);
}
}

View File

@@ -0,0 +1,205 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {IDeprecatedCollectModule} from '../../../../interfaces/IDeprecatedCollectModule.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 followerOnly Whether only followers should be able to collect.
*/
struct ProfilePublicationData {
uint256 collectLimit;
uint256 currentCollects;
uint256 amount;
address currency;
address recipient;
uint16 referralFee;
bool followerOnly;
}
/**
* @title LimitedFeeCollectModule
* @author Lens Protocol
*
* @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 DeprecatedLimitedFeeCollectModule is FeeModuleBase, FollowValidationModuleBase, IDeprecatedCollectModule {
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 profile ID of the publication to initialize this module for's publishing profile.
* @param pubId The publication ID of the publication to initialize this module for.
* @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.
* bool followerOnly: Whether only followers should be able to collect.
*
* @return bytes 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,
bool followerOnly
) = abi.decode(data, (uint256, uint256, address, address, uint16, bool));
if (
collectLimit == 0 ||
!_currencyWhitelisted(currency) ||
recipient == address(0) ||
referralFee > BPS_MAX ||
amount == 0
) 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].followerOnly = followerOnly;
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 {
if (_dataByPublicationByProfile[profileId][pubId].followerOnly)
_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 ProfilePublicationData 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);
if (treasuryAmount > 0)
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);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
}
}

View File

@@ -0,0 +1,228 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {IDeprecatedCollectModule} from '../../../../interfaces/IDeprecatedCollectModule.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.
* @param followerOnly Whether only followers should be able to collect.
*/
struct ProfilePublicationData {
uint256 collectLimit;
uint256 currentCollects;
uint256 amount;
address currency;
address recipient;
uint16 referralFee;
bool followerOnly;
uint40 endTimestamp;
}
/**
* @title LimitedTimedFeeCollectModule
* @author Lens Protocol
*
* @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 DeprecatedLimitedTimedFeeCollectModule is FeeModuleBase, FollowValidationModuleBase, IDeprecatedCollectModule {
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 profileId The profile ID of the publication to initialize this module for's publishing profile.
* @param pubId The publication ID of the publication to initialize this module for.
* @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.
* bool followerOnly: Whether only followers should be able to collect.
*
* @return bytes 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) {
unchecked {
uint40 endTimestamp = uint40(block.timestamp) + ONE_DAY;
(
uint256 collectLimit,
uint256 amount,
address currency,
address recipient,
uint16 referralFee,
bool followerOnly
) = abi.decode(data, (uint256, uint256, address, address, uint16, bool));
if (
collectLimit == 0 ||
!_currencyWhitelisted(currency) ||
recipient == address(0) ||
referralFee > BPS_MAX ||
amount == 0
) 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].followerOnly = followerOnly;
_dataByPublicationByProfile[profileId][pubId].endTimestamp = endTimestamp;
return
abi.encode(
collectLimit,
amount,
currency,
recipient,
referralFee,
followerOnly,
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 {
if (_dataByPublicationByProfile[profileId][pubId].followerOnly)
_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 ProfilepublicationData 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);
if (treasuryAmount > 0)
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);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
}
}

View File

@@ -0,0 +1,41 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {IDeprecatedCollectModule} from '../../../../interfaces/IDeprecatedCollectModule.sol';
import {Errors} from '../../../../libraries/Errors.sol';
/**
* @title RevertCollectModule
* @author Lens Protocol
*
* @notice This is a simple Lens CollectModule implementation, inheriting from the ICollectModule interface.
*
* This module works by disallowing all collects.
*/
contract DeprecatedRevertCollectModule is IDeprecatedCollectModule {
/**
* @dev There is nothing needed at initialization.
*/
function initializePublicationCollectModule(
uint256,
uint256,
bytes calldata
) external pure override returns (bytes memory) {
return new bytes(0);
}
/**
* @dev Processes a collect by:
* 1. Always reverting
*/
function processCollect(
uint256,
address,
uint256,
uint256,
bytes calldata
) external pure override {
revert Errors.CollectNotAllowed();
}
}

View File

@@ -0,0 +1,205 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {IDeprecatedCollectModule} from '../../../../interfaces/IDeprecatedCollectModule.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.
* @param followerOnly Whether only followers should be able to collect.
*/
struct ProfilePublicationData {
uint256 amount;
address currency;
address recipient;
uint16 referralFee;
bool followerOnly;
uint40 endTimestamp;
}
/**
* @title TimedFeeCollectModule
* @author Lens Protocol
*
* @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 DeprecatedTimedFeeCollectModule is FeeModuleBase, FollowValidationModuleBase, IDeprecatedCollectModule {
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 profileId The profile ID of the publication to initialize this module for's publishing profile.
* @param pubId The publication ID of the publication to initialize this module for.
* @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.
* bool followerOnly: Whether only followers should be able to collect.
*
* @return bytes 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) {
unchecked {
uint40 endTimestamp = uint40(block.timestamp) + ONE_DAY;
(
uint256 amount,
address currency,
address recipient,
uint16 referralFee,
bool followerOnly
) = abi.decode(data, (uint256, address, address, uint16, bool));
if (
!_currencyWhitelisted(currency) ||
recipient == address(0) ||
referralFee > BPS_MAX ||
amount == 0
) 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].followerOnly = followerOnly;
_dataByPublicationByProfile[profileId][pubId].endTimestamp = endTimestamp;
return abi.encode(amount, currency, recipient, referralFee, followerOnly, 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 {
if (_dataByPublicationByProfile[profileId][pubId].followerOnly)
_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 ProfilePublicationData 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);
if (treasuryAmount > 0)
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);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
}
}

View File

@@ -0,0 +1,147 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {Errors} from '../../../../libraries/Errors.sol';
import {Events} from '../../../../libraries/Events.sol';
import {ModuleBase} from '../../ModuleBase.sol';
import {DeprecatedFollowValidatorFollowModuleBase} from './DeprecatedFollowValidatorFollowModuleBase.sol';
import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
/**
* @title ApprovalFollowModule
* @author Lens Protocol
*
* @notice This follow module only allows addresses that are approved for a profile by the profile owner to follow.
*/
contract DeprecatedApprovalFollowModule is DeprecatedFollowValidatorFollowModuleBase {
// 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();
uint256 addressesLength = addresses.length;
for (uint256 i = 0; i < addressesLength; ) {
_approvedByProfileByOwner[owner][profileId][addresses[i]] = toApprove[i];
unchecked {
++i;
}
}
emit Events.FollowsApproved(owner, profileId, addresses, toApprove, block.timestamp);
}
/**
* @notice This follow module works on custom profile owner approvals.
*
* @param profileId The profile ID of the profile to initialize this module for.
* @param data The arbitrary data parameter, decoded into:
* address[] addresses: The array of addresses to approve initially.
*
* @return bytes 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[]));
uint256 addressesLength = addresses.length;
for (uint256 i = 0; i < addressesLength; ) {
_approvedByProfileByOwner[owner][profileId][addresses[i]] = true;
unchecked {
++i;
}
}
}
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
) 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 bool True if the address is approved and false otherwise.
*/
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.
*
* @return bool[] true if the address at the specified index is approved and false otherwise.
*/
function isApprovedArray(
address profileOwner,
uint256 profileId,
address[] calldata toCheck
) external view returns (bool[] memory) {
bool[] memory approved = new bool[](toCheck.length);
uint256 toCheckLength = toCheck.length;
for (uint256 i = 0; i < toCheckLength; ) {
approved[i] = _approvedByProfileByOwner[profileOwner][profileId][toCheck[i]];
unchecked {
++i;
}
}
return approved;
}
}

View File

@@ -0,0 +1,115 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {ILensHub} from '../../../../interfaces/ILensHub.sol';
import {Errors} from '../../../../libraries/Errors.sol';
import {FeeModuleBase} from '../../FeeModuleBase.sol';
import {ModuleBase} from '../../ModuleBase.sol';
import {DeprecatedFollowValidatorFollowModuleBase} from './DeprecatedFollowValidatorFollowModuleBase.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 Protocol
*
* @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 DeprecatedFeeFollowModule is FeeModuleBase, DeprecatedFollowValidatorFollowModuleBase {
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 profileId The profile ID of the profile to initialize this module for.
* @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 bytes 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 == 0)
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);
if (treasuryAmount > 0)
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 ProfileData The ProfileData struct mapped to that profile.
*/
function getProfileData(uint256 profileId) external view returns (ProfileData memory) {
return _dataByProfile[profileId];
}
}

View File

@@ -0,0 +1,38 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {IDeprecatedFollowModule} from '../../../../interfaces/IDeprecatedFollowModule.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 Protocol
*
* @notice This abstract contract adds the default expected behavior for follow validation in a follow module
* to inheriting contracts.
*/
abstract contract DeprecatedFollowValidatorFollowModuleBase is ModuleBase, IDeprecatedFollowModule {
/**
* @notice Standard function to validate follow NFT ownership. This module is agnostic to follow NFT token IDs
* and other properties.
*/
function isFollowing(
uint256 profileId,
address follower,
uint256 followNFTTokenId
) external view override returns (bool) {
address followNFT = ILensHub(HUB).getFollowNFT(profileId);
if (followNFT == address(0)) {
return false;
} else {
return
followNFTTokenId == 0
? IERC721(followNFT).balanceOf(follower) != 0
: IERC721(followNFT).ownerOf(followNFTTokenId) == follower;
}
}
}

View File

@@ -0,0 +1,70 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {Errors} from '../../../../libraries/Errors.sol';
import {ModuleBase} from '../../ModuleBase.sol';
import {DeprecatedFollowValidatorFollowModuleBase} from './DeprecatedFollowValidatorFollowModuleBase.sol';
import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
/**
* @title ProfileFollowModule
* @author Lens Protocol
*
* @notice A Lens Profile NFT token-gated follow module with single follow per token validation.
*/
contract DeprecatedProfileFollowModule is DeprecatedFollowValidatorFollowModuleBase {
/**
* Given two profile IDs tells if the former has already been used to follow the latter.
*/
mapping(uint256 => mapping(uint256 => bool)) public isProfileFollowing;
constructor(address hub) ModuleBase(hub) {}
/**
* @notice This follow module allows users to follow using a profile once.
*
* @return bytes Empty bytes.
*/
function initializeFollowModule(uint256, bytes calldata)
external
view
override
onlyHub
returns (bytes memory)
{
return new bytes(0);
}
/**
* @dev Processes a follow by:
* 1. Validating that the follower owns the profile passed through the data param.
* 2. Validating that the profile that is being used to execute the follow was not already used for following the
* given profile.
*/
function processFollow(
address follower,
uint256 profileId,
bytes calldata data
) external override onlyHub {
uint256 followerProfileId = abi.decode(data, (uint256));
if (IERC721(HUB).ownerOf(followerProfileId) != follower) {
revert Errors.NotProfileOwner();
}
if (isProfileFollowing[followerProfileId][profileId]) {
revert Errors.FollowInvalid();
} else {
isProfileFollowing[followerProfileId][profileId] = true;
}
}
/**
* @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 {}
}

View File

@@ -0,0 +1,53 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {Errors} from '../../../../libraries/Errors.sol';
import {ModuleBase} from '../../ModuleBase.sol';
import {DeprecatedFollowValidatorFollowModuleBase} from './DeprecatedFollowValidatorFollowModuleBase.sol';
/**
* @title RevertFollowModule
* @author Lens Protocol
*
* @notice This follow module rejects all follow attempts.
*/
contract DeprecatedRevertFollowModule is DeprecatedFollowValidatorFollowModuleBase {
constructor(address hub) ModuleBase(hub) {}
/**
* @notice This follow module always reverts.
*
* @return bytes Empty bytes.
*/
function initializeFollowModule(uint256, bytes calldata)
external
view
override
onlyHub
returns (bytes memory)
{
return new bytes(0);
}
/**
* @dev Processes a follow by rejecting it reverting the transaction.
*/
function processFollow(
address,
uint256,
bytes calldata
) external view override onlyHub {
revert Errors.FollowInvalid();
}
/**
* @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 {}
}

View File

@@ -0,0 +1,60 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {IDeprecatedReferenceModule} from '../../../../interfaces/IDeprecatedReferenceModule.sol';
import {ModuleBase} from '../../ModuleBase.sol';
import {FollowValidationModuleBase} from '../../FollowValidationModuleBase.sol';
import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
/**
* @title FollowerOnlyReferenceModule
* @author Lens Protocol
*
* @notice A simple reference module that validates that comments or mirrors originate from a profile owned
* by a follower.
*/
contract DeprecatedFollowerOnlyReferenceModule is FollowValidationModuleBase, IDeprecatedReferenceModule {
constructor(address hub) ModuleBase(hub) {}
/**
* @dev There is nothing needed at initialization.
*/
function initializeReferenceModule(
uint256,
uint256,
bytes calldata
) 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,
bytes calldata
) external view override {
address commentCreator = IERC721(HUB).ownerOf(profileId);
_checkFollowValidity(profileIdPointed, commentCreator);
}
/**
* @notice Validates that the mirroring 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,
bytes calldata
) external view override {
address mirrorCreator = IERC721(HUB).ownerOf(profileId);
_checkFollowValidity(profileIdPointed, mirrorCreator);
}
}

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {IFollowModule} from '../../../interfaces/IFollowModule.sol';
import {Errors} from '../../../libraries/Errors.sol';
@@ -58,12 +58,11 @@ contract ApprovalFollowModule is FollowValidatorFollowModuleBase {
*
* @return bytes 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)
{
function initializeFollowModule(
uint256 profileId,
address,
bytes calldata data
) external override onlyHub returns (bytes memory) {
address owner = IERC721(HUB).ownerOf(profileId);
if (data.length > 0) {
@@ -84,9 +83,11 @@ contract ApprovalFollowModule is FollowValidatorFollowModuleBase {
* 1. Validating that the follower has been approved for that profile by the profile owner
*/
function processFollow(
uint256,
address follower,
address,
uint256 profileId,
bytes calldata data
bytes calldata
) external override onlyHub {
address owner = IERC721(HUB).ownerOf(profileId);
if (!_approvedByProfileByOwner[owner][profileId][follower])

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {IFollowModule} from '../../../interfaces/IFollowModule.sol';
import {ILensHub} from '../../../interfaces/ILensHub.sol';
@@ -50,12 +50,11 @@ contract FeeFollowModule is FeeModuleBase, FollowValidatorFollowModuleBase {
*
* @return bytes 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)
{
function initializeFollowModule(
uint256 profileId,
address,
bytes calldata data
) external override onlyHub returns (bytes memory) {
(uint256 amount, address currency, address recipient) = abi.decode(
data,
(uint256, address, address)
@@ -74,7 +73,9 @@ contract FeeFollowModule is FeeModuleBase, FollowValidatorFollowModuleBase {
* 1. Charging a fee
*/
function processFollow(
address follower,
uint256,
address,
address executor,
uint256 profileId,
bytes calldata data
) external override onlyHub {
@@ -87,9 +88,9 @@ contract FeeFollowModule is FeeModuleBase, FollowValidatorFollowModuleBase {
uint256 treasuryAmount = (amount * treasuryFee) / BPS_MAX;
uint256 adjustedAmount = amount - treasuryAmount;
IERC20(currency).safeTransferFrom(follower, recipient, adjustedAmount);
IERC20(currency).safeTransferFrom(executor, recipient, adjustedAmount);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(follower, treasury, treasuryAmount);
IERC20(currency).safeTransferFrom(executor, treasury, treasuryAmount);
}
/**

View File

@@ -1,8 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {IFollowModule} from '../../../interfaces/IFollowModule.sol';
import {IFollowModuleLegacy} from '../../../interfaces/IFollowModuleLegacy.sol';
import {ILensHub} from '../../../interfaces/ILensHub.sol';
import {Errors} from '../../../libraries/Errors.sol';
import {ModuleBase} from '../ModuleBase.sol';
@@ -15,12 +15,13 @@ import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
* @notice This abstract contract adds the default expected behavior for follow validation in a follow module
* to inheriting contracts.
*/
abstract contract FollowValidatorFollowModuleBase is ModuleBase, IFollowModule {
abstract contract FollowValidatorFollowModuleBase is ModuleBase, IFollowModuleLegacy {
/**
* @notice Standard function to validate follow NFT ownership. This module is agnostic to follow NFT token IDs
* and other properties.
*/
function isFollowing(
uint256,
uint256 profileId,
address follower,
uint256 followNFTTokenId

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {IFollowModule} from '../../../interfaces/IFollowModule.sol';
import {Errors} from '../../../libraries/Errors.sol';
@@ -25,17 +25,13 @@ contract ProfileFollowModule is FollowValidatorFollowModuleBase {
/**
* @notice This follow module works on custom profile owner approvals.
*
* @param profileId The profile ID of the profile to initialize this module for.
* @param data The arbitrary data parameter, which in this particular module initialization will be just ignored.
*
* @return bytes Empty bytes.
*/
function initializeFollowModule(uint256 profileId, bytes calldata data)
external
override
onlyHub
returns (bytes memory)
{
function initializeFollowModule(
uint256,
address,
bytes calldata
) external view override onlyHub returns (bytes memory) {
return new bytes(0);
}
@@ -46,7 +42,9 @@ contract ProfileFollowModule is FollowValidatorFollowModuleBase {
* given profile.
*/
function processFollow(
uint256,
address follower,
address,
uint256 profileId,
bytes calldata data
) external override onlyHub {

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {Errors} from '../../../libraries/Errors.sol';
import {ModuleBase} from '../ModuleBase.sol';
@@ -16,30 +16,27 @@ contract RevertFollowModule is FollowValidatorFollowModuleBase {
constructor(address hub) ModuleBase(hub) {}
/**
* @notice This follow module works on custom profile owner approvals.
*
* @param profileId The profile ID of the profile to initialize this module for.
* @param data The arbitrary data parameter, which in this particular module initialization will be just ignored.
* @notice This follow module always reverts.
*
* @return bytes Empty bytes.
*/
function initializeFollowModule(uint256 profileId, bytes calldata data)
external
view
override
onlyHub
returns (bytes memory)
{
function initializeFollowModule(
uint256,
address,
bytes calldata
) external view override onlyHub returns (bytes memory) {
return new bytes(0);
}
/**
* @dev Processes a follow by rejecting it reverting the transaction.
* @dev Processes a follow by rejecting it and reverting the transaction.
*/
function processFollow(
address follower,
uint256 profileId,
bytes calldata data
uint256,
address,
address,
uint256,
bytes calldata
) external view override onlyHub {
revert Errors.FollowInvalid();
}

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {IReferenceModule} from '../../../interfaces/IReferenceModule.sol';
import {ModuleBase} from '../ModuleBase.sol';
@@ -21,9 +21,10 @@ contract FollowerOnlyReferenceModule is FollowValidationModuleBase, IReferenceMo
* @dev There is nothing needed at initialization.
*/
function initializeReferenceModule(
uint256 profileId,
uint256 pubId,
bytes calldata data
uint256,
address,
uint256,
bytes calldata
) external pure override returns (bytes memory) {
return new bytes(0);
}
@@ -35,24 +36,26 @@ contract FollowerOnlyReferenceModule is FollowValidationModuleBase, IReferenceMo
*/
function processComment(
uint256 profileId,
address,
uint256 profileIdPointed,
uint256 pubIdPointed,
bytes calldata data
uint256,
bytes calldata
) external view override {
address commentCreator = IERC721(HUB).ownerOf(profileId);
_checkFollowValidity(profileIdPointed, commentCreator);
}
/**
* @notice Validates that the commenting profile's owner is a follower.
* @notice Validates that the mirroring 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,
address,
uint256 profileIdPointed,
uint256 pubIdPointed,
bytes calldata data
uint256,
bytes calldata
) external view override {
address mirrorCreator = IERC721(HUB).ownerOf(profileId);
_checkFollowValidity(profileIdPointed, mirrorCreator);

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {DataTypes} from '../../libraries/DataTypes.sol';
@@ -13,60 +13,24 @@ import {DataTypes} from '../../libraries/DataTypes.sol';
* storage variables should be done solely at the bottom of this contract.
*/
abstract contract LensHubStorage {
bytes32 internal constant SET_DEFAULT_PROFILE_WITH_SIG_TYPEHASH =
keccak256(
'SetDefaultProfileWithSig(address wallet,uint256 profileId,uint256 nonce,uint256 deadline)'
);
bytes32 internal constant SET_FOLLOW_MODULE_WITH_SIG_TYPEHASH =
keccak256(
'SetFollowModuleWithSig(uint256 profileId,address followModule,bytes followModuleInitData,uint256 nonce,uint256 deadline)'
);
bytes32 internal constant SET_FOLLOW_NFT_URI_WITH_SIG_TYPEHASH =
keccak256(
'SetFollowNFTURIWithSig(uint256 profileId,string followNFTURI,uint256 nonce,uint256 deadline)'
);
bytes32 internal constant SET_DISPATCHER_WITH_SIG_TYPEHASH =
keccak256(
'SetDispatcherWithSig(uint256 profileId,address dispatcher,uint256 nonce,uint256 deadline)'
);
bytes32 internal constant SET_PROFILE_IMAGE_URI_WITH_SIG_TYPEHASH =
keccak256(
'SetProfileImageURIWithSig(uint256 profileId,string imageURI,uint256 nonce,uint256 deadline)'
);
bytes32 internal constant POST_WITH_SIG_TYPEHASH =
keccak256(
'PostWithSig(uint256 profileId,string contentURI,address collectModule,bytes collectModuleInitData,address referenceModule,bytes referenceModuleInitData,uint256 nonce,uint256 deadline)'
);
bytes32 internal constant COMMENT_WITH_SIG_TYPEHASH =
keccak256(
'CommentWithSig(uint256 profileId,string contentURI,uint256 profileIdPointed,uint256 pubIdPointed,bytes referenceModuleData,address collectModule,bytes collectModuleInitData,address referenceModule,bytes referenceModuleInitData,uint256 nonce,uint256 deadline)'
);
bytes32 internal constant MIRROR_WITH_SIG_TYPEHASH =
keccak256(
'MirrorWithSig(uint256 profileId,uint256 profileIdPointed,uint256 pubIdPointed,bytes referenceModuleData,address referenceModule,bytes referenceModuleInitData,uint256 nonce,uint256 deadline)'
);
bytes32 internal constant FOLLOW_WITH_SIG_TYPEHASH =
keccak256(
'FollowWithSig(uint256[] profileIds,bytes[] datas,uint256 nonce,uint256 deadline)'
);
bytes32 internal constant COLLECT_WITH_SIG_TYPEHASH =
keccak256(
'CollectWithSig(uint256 profileId,uint256 pubId,bytes data,uint256 nonce,uint256 deadline)'
);
mapping(address => bool) internal _profileCreatorWhitelisted; // Slot 13
mapping(address => bool) internal _followModuleWhitelisted; // Slot 14
mapping(address => bool) internal _collectModuleWhitelisted; // Slot 15
mapping(address => bool) internal _referenceModuleWhitelisted; // Slot 16
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; // Slot 17
mapping(bytes32 => uint256) internal _profileIdByHandleHash; // Slot 18
mapping(uint256 => DataTypes.ProfileStruct) internal _profileById; // Slot 19
mapping(uint256 => mapping(uint256 => DataTypes.PublicationStruct)) internal _pubByIdByProfile; // Slot 20
mapping(uint256 => address) internal _dispatcherByProfile;
mapping(bytes32 => uint256) internal _profileIdByHandleHash;
mapping(uint256 => DataTypes.ProfileStruct) internal _profileById;
mapping(uint256 => mapping(uint256 => DataTypes.PublicationStruct)) internal _pubByIdByProfile;
mapping(address => uint256) internal _defaultProfileByAddress; // Slot 21
mapping(address => uint256) internal _defaultProfileByAddress;
uint256 internal _profileCounter; // Slot 22 - this is different to TotalSupply, as TotalSupply is decreased when the Profile is burned
address internal _governance; // Slot 23
address internal _emergencyAdmin; // Slot 24
uint256 internal _profileCounter;
address internal _governance;
address internal _emergencyAdmin;
// Slots introduced by Lens V2 upgrade.
mapping(address => mapping(address => bool)) internal _delegatedExecutorApproval; // Slot 25
mapping(uint256 => string) internal _metadataByProfile; // Slot 26
mapping(uint256 => mapping(uint256 => bool)) internal _blockedStatus; // Slot 27, _blockedStatus[byProfile][profile]
}

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
/**
* @title ICollectModule
@@ -13,6 +13,7 @@ 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 executor The owner or an approved delegated executor.
* @param pubId The associated publication's LensHub publication ID.
* @param data Arbitrary data __passed from the user!__ to be decoded.
*
@@ -21,6 +22,7 @@ interface ICollectModule {
*/
function initializePublicationCollectModule(
uint256 profileId,
address executor,
uint256 pubId,
bytes calldata data
) external returns (bytes memory);
@@ -29,15 +31,19 @@ interface ICollectModule {
* @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 collectorProfileId The LensHub profile token ID of the collector's profile.
* @param collectorProfileOwner The collector address.
* @param executor The collector or an approved delegated executor.
* @param publisherProfileId 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 collectorProfileId,
address collectorProfileOwner,
address executor,
uint256 publisherProfileId,
uint256 pubId,
bytes calldata data
) external;

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
/**
* @title ICollectNFT

View File

@@ -0,0 +1,44 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
/**
* @title ICollectModule
* @author Lens Protocol
*
* @notice This is the deprecated interface for previously Lens-compatible CollectModules.
*/
interface IDeprecatedCollectModule {
/**
* @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 bytes 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;
}

View File

@@ -0,0 +1,85 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
/**
* @title IFollowModule
* @author Lens Protocol
*
* @notice This is the deprecated interface for previously Lens-compatible FollowModules.
*/
interface IDeprecatedFollowModule {
/**
* @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.
*
* @return bytes The encoded data to emit in the hub.
*/
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 `isFollowing` passing the profile ID, follower & follower token ID and checks it returned true.
* 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.
*
* @return true if the given address is following the given profile ID, false otherwise.
*/
function isFollowing(
uint256 profileId,
address follower,
uint256 followNFTTokenId
) external view returns (bool);
}

View File

@@ -0,0 +1,57 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
/**
* @title IReferenceModule
* @author Lens Protocol
*
* @notice This is the deprecated interface for previously Lens-compatible ReferenceModules.
*/
interface IDeprecatedReferenceModule {
/**
* @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 bytes 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.
* @param data Arbitrary data __passed from the commenter!__ to be decoded.
*/
function processComment(
uint256 profileId,
uint256 profileIdPointed,
uint256 pubIdPointed,
bytes calldata data
) 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.
* @param data Arbitrary data __passed from the mirrorer!__ to be decoded.
*/
function processMirror(
uint256 profileId,
uint256 profileIdPointed,
uint256 pubIdPointed,
bytes calldata data
) external;
}

View File

@@ -0,0 +1,9 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
interface IEIP1271Implementer {
function isValidSignature(bytes32 _hash, bytes memory _signature)
external
view
returns (bytes4);
}

View File

@@ -2,7 +2,7 @@
pragma solidity ^0.8.0;
import '@openzeppelin/contracts/token/ERC721/IERC721.sol';
import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
/**
* @title IERC721Time

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
/**
* @title IFollowModule
@@ -13,72 +13,32 @@ 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 executor The owner or an approved delegated executor.
* @param data Arbitrary data passed by the profile creator.
*
* @return bytes The encoded data to emit in the hub.
*/
function initializeFollowModule(uint256 profileId, bytes calldata data)
external
returns (bytes memory);
function initializeFollowModule(
uint256 profileId,
address executor,
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 followerProfileId The LensHub profile token ID of the follower's profile (currently unused, preemptive interface upgrade).
* @param followTokenId The ID of the follow token used to follow. Zero if a new one was minted, in this case, the follow ID assigned
* can be queried from the Follow NFT collection if needed.
* @param executor The follower or an approved delegated executor.
* @param profileId The token ID of the profile being followed.
* @param data Arbitrary data passed by the follower.
*/
function processFollow(
address follower,
uint256 followerProfileId,
uint256 followTokenId,
address executor,
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 `isFollowing` passing the profile ID, follower & follower token ID and checks it returned true.
* 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.
*
* @return true if the given address is following the given profile ID, false otherwise.
*/
function isFollowing(
uint256 profileId,
address follower,
uint256 followNFTTokenId
) external view returns (bool);
}

View File

@@ -0,0 +1,93 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
/**
* @title IFollowModuleLegacy
* @author Lens Protocol
*
* @notice This is the standard interface for all Lens-compatible FollowModules.
*/
interface IFollowModuleLegacy {
/**
* @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 executor The owner or an approved delegated executor.
* @param data Arbitrary data passed by the profile creator.
*
* @return bytes The encoded data to emit in the hub.
*/
function initializeFollowModule(
uint256 profileId,
address executor,
bytes calldata data
) external returns (bytes memory);
/**
* @notice Processes a given follow, this can only be called from the LensHub contract.
*
* @param followerProfileId The LensHub profile token ID of the follower's profile (currently unused, preemptive interface upgrade).
* @param follower The follower address.
* @param executor The follower or an approved delegated executor.
* @param profileId The token ID of the profile being followed.
* @param data Arbitrary data passed by the follower.
*/
function processFollow(
uint256 followerProfileId,
address follower,
address executor,
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 `isFollowing` passing the profile ID, follower & follower token ID and checks it returned true.
* 2. The follow module:
* - Validates the subscription status for that given NFT, reverting on an invalid subscription.
*
* @param followerProfileId The LensHub profile token ID of the follower's profile (currently unused, preemptive interface upgrade).
* @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.
*
* @return true if the given address is following the given profile ID, false otherwise.
*/
function isFollowing(
uint256 followerProfileId,
uint256 profileId,
address follower,
uint256 followNFTTokenId
) external view returns (bool);
}

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {DataTypes} from '../libraries/DataTypes.sol';
@@ -11,60 +11,183 @@ import {DataTypes} from '../libraries/DataTypes.sol';
* @notice This is the interface for the FollowNFT contract, which is cloned upon the first follow for any profile.
*/
interface IFollowNFT {
error AlreadyFollowing();
error NotFollowing();
error FollowTokenDoesNotExist();
error AlreadyWrapped();
error OnlyWrappedFollowTokens();
error DoesNotHavePermissions();
/**
* @notice Initializes the follow NFT, setting the hub as the privileged minter and storing the associated profile ID.
* @notice A struct containing token follow-related data.
*
* @param profileId The token ID of the profile in the hub associated with this followNFT, used for transfer hooks.
* @param followerProfileId The ID of the profile using the token to follow.
* @param originalFollowTimestamp The timestamp of the first follow performed with the token.
* @param followTimestamp The timestamp of the current follow, if a profile is using the token to follow.
* @param profileIdAllowedToRecover The ID of the profile allowed to recover the follow ID, if any.
*/
struct FollowData {
uint160 followerProfileId;
uint48 originalFollowTimestamp;
uint48 followTimestamp;
uint256 profileIdAllowedToRecover;
}
/**
* @notice Initializes the follow NFT.
*
* @dev Sets the hub as priviliged sender, the targeted profile, and the token royalties.
*
* @param profileId The ID of the profile targeted by the follow tokens minted by this collection.
*/
function initialize(uint256 profileId) external;
/**
* @notice Mints a follow NFT to the specified address. This can only be called by the hub, and is called
* upon follow.
* @notice Makes the passed profile to follow the profile targeted in this contract.
*
* @param to The address to mint the NFT to.
* @dev This must be only callable by the LensHub contract.
*
* @return uint256 An interger representing the minted token ID.
* @param followerProfileId The ID of the profile acting as the follower.
* @param executor The address executing the operation, which is the signer in case of using meta-transactions or
* the sender otherwise.
* @param followTokenId The ID of the follow token to be used for this follow operation. Zero if a new follow token
* should be minted.
*
* @return uint256 The ID of the token used to follow.
*/
function mint(address to) external returns (uint256);
function follow(
uint256 followerProfileId,
address executor,
uint256 followTokenId
) external returns (uint256);
/**
* @notice Delegates the caller's governance power to the given delegatee address.
* @notice Makes the passed profile to unfollow the profile targeted in this contract.
*
* @param delegatee The delegatee address to delegate governance power to.
* @dev This must be only callable by the LensHub contract.
*
* @param unfollowerProfileId The ID of the profile that is perfrorming the unfollow operation.
* @param executor The address executing the operation, which is the signer in case of using meta-transactions or
* the sender otherwise.
*/
function delegate(address delegatee) external;
function unfollow(uint256 unfollowerProfileId, address executor) external;
/**
* @notice Delegates the delegator's governance power via meta-tx to the given delegatee address.
* @notice Removes the follower from the given follow NFT.
*
* @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.
* @dev It can only be called over wrapped tokens, by their owner or an approved-for-all address.
*
* @param followTokenId The ID of the follow token to remove the follower from.
*/
function delegateBySig(
address delegator,
address delegatee,
DataTypes.EIP712Signature calldata sig
) external;
function removeFollower(uint256 followTokenId) external;
/**
* @notice Returns the governance power for a given user at a specified block number.
* @notice Approves the given profile to follow with the given wrapped token.
*
* @param user The user to query governance power for.
* @param blockNumber The block number to query the user's governance power at.
* @dev It approves setting a follower on the given wrapped follow token, which lets the follow token owner to allow
* a profile to follow with his token without losing its ownership. This approval is cleared on transfers, as well
* as when unwrapping.
*
* @return uint256 The power of the given user at the given block number.
* @param approvedProfileId The ID of the profile approved to follow with the given token.
* @param followTokenId The ID of the follow token to be approved for the given profile.
*/
function getPowerByBlockNumber(address user, uint256 blockNumber) external returns (uint256);
function approveFollow(uint256 approvedProfileId, uint256 followTokenId) external;
/**
* @notice Returns the total delegated supply at a specified block number. This is the sum of all
* current available voting power at a given block.
* @notice Unties the follow token from the follower's profile one, and wraps it into the ERC-721 untied follow
* tokens collection. Untied follow tokens will NOT be automatically transferred with their follower profile.
*
* @param blockNumber The block number to query the delegated supply at.
*
* @return uint256 The delegated supply at the given block number.
* @param followTokenId The ID of the follow token to untie and wrap.
*/
function getDelegatedSupplyByBlockNumber(uint256 blockNumber) external returns (uint256);
function wrap(uint256 followTokenId) external;
/**
* @notice Unwraps the follow token from the ERC-721 untied follow tokens collection, and ties it to the follower's
* profile token. Tokens that are tied to the follower profile will be automatically transferred with it.
*
* @param followTokenId The ID of the follow token to unwrap and tie to its follower.
*/
function unwrap(uint256 followTokenId) external;
/**
* @notice Processes logic when the given profile is being blocked. If it was following the targeted profile,
* this will make it to unfollow.
*
* @dev This must be only callable by the LensHub contract.
*
* @param followerProfileId The ID of the follow token to unwrap and tie.
*/
function processBlock(uint256 followerProfileId) external;
/**
* @notice Gets the ID of the profile following with the given follow token.
*
* @param followTokenId The ID of the follow token whose follower should be queried.
*
* @return uint256 The ID of the profile following with the given token, zero if it is not being used to follow.
*/
function getFollowerProfileId(uint256 followTokenId) external view returns (uint256);
/**
* @notice Gets the original follow timestamp of the given follow token.
*
* @param followTokenId The ID of the follow token whose original follow timestamp should be queried.
*
* @return uint256 The timestamp of the first follow performed with the token, zero if was not used to follow yet.
*/
function getOriginalFollowTimestamp(uint256 followTokenId) external view returns (uint256);
/**
* @notice Gets the current follow timestamp of the given follow token.
*
* @param followTokenId The ID of the follow token whose follow timestamp should be queried.
*
* @return uint256 The timestamp of the current follow of the token, zero if it is not being used to follow.
*/
function getFollowTimestamp(uint256 followTokenId) external view returns (uint256);
/**
* @notice Gets the ID of the profile allowed to recover the given follow token.
*
* @param followTokenId The ID of the follow token whose allowed profile to recover should be queried.
*
* @return uint256 The ID of the profile allowed to recover the given follow token, zero if none of them is allowed.
*/
function getProfileIdAllowedToRecover(uint256 followTokenId) external view returns (uint256);
/**
* @notice Gets the follow data of the given follow token.
*
* @param followTokenId The ID of the follow token whose follow data should be queried.
*
* @return FollowData The token data associated with the given follow token.
*/
function getFollowData(uint256 followTokenId) external view returns (FollowData memory);
/**
* @notice Tells if the given profile is following the profile targeted in this contract.
*
* @param followerProfileId The ID of the profile whose following state should be queried.
*
* @return uint256 The ID of the profile set as follower in the given token, zero if it is not being used to follow.
*/
function isFollowing(uint256 followerProfileId) external view returns (bool);
/**
* @notice Gets the ID of the token being used to follow by the given follower.
*
* @param followerProfileId The ID of the profile whose follow ID should be queried.
*
* @return uint256 The ID of the token being used to follow by the given follower, zero if he is not following.
*/
function getFollowTokenId(uint256 followerProfileId) external view returns (uint256);
/**
* @notice Gets the ID of the profile approved to follow with the given token.
*
* @param followTokenId The ID of the token whose approved to follow should be queried.
*
* @return uint256 The ID of the profile approved to follow with the given token, zero if none of them is approved.
*/
function getFollowApproved(uint256 followTokenId) external view returns (uint256);
}

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {DataTypes} from '../libraries/DataTypes.sol';
@@ -96,7 +96,6 @@ interface ILensHub {
*
* @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.
* followModuleInitData: The follow module initialization data, if any.
@@ -104,14 +103,17 @@ interface ILensHub {
function createProfile(DataTypes.CreateProfileData calldata vars) external returns (uint256);
/**
* @notice Sets the mapping between wallet and its main profile identity.
* @notice Sets the mapping between wallet and its main profile identity. Must be called either by the wallet or a
* delegated executor.
*
* @param onBehalfOf The address to set the default profile on behalf of.
* @param profileId The token ID of the profile to set as the main profile identity.
*/
function setDefaultProfile(uint256 profileId) external;
function setDefaultProfile(address onBehalfOf, uint256 profileId) external;
/**
* @notice Sets the mapping between wallet and its main profile identity via signature with the specified parameters.
* @notice Sets the mapping between wallet and its main profile identity via signature with the specified parameters. The
* signer must either be the profile owner or a delegated executor.
*
* @param vars A SetDefaultProfileWithSigData struct, including the regular parameters and an EIP712Signature struct.
*/
@@ -119,7 +121,25 @@ interface ILensHub {
external;
/**
* @notice Sets a profile's follow module, must be called by the profile owner.
* @notice Sets the metadata URI for the given profile. Must be called either from the profile owner, a delegated
* executor, or the profile's dispatcher.
*
* @param profileId The token ID of the profile to set the metadata URI for.
* @param metadataURI The metadata URI to set for the given profile.
*/
function setProfileMetadataURI(uint256 profileId, string calldata metadataURI) external;
/**
* @notice Sets the metadata URI via signature for the given profile with the specified parameters. The signer must
* either be the profile owner or a delegated executor.
*
* @param vars A SetProfileMetadataURIWithSigData struct, including the regular parameters and an EIP712Signature struct.
*/
function setProfileMetadataURIWithSig(DataTypes.SetProfileMetadataURIWithSigData calldata vars)
external;
/**
* @notice Sets the follow module for the given profile. 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.
@@ -132,7 +152,8 @@ interface ILensHub {
) external;
/**
* @notice Sets a profile's follow module via signature with the specified parameters.
* @notice Sets the follow module via signature for the given profile with the specified parameters. The signer must
* either be the profile owner or a delegated executor.
*
* @param vars A SetFollowModuleWithSigData struct, including the regular parameters and an EIP712Signature struct.
*/
@@ -154,7 +175,25 @@ interface ILensHub {
function setDispatcherWithSig(DataTypes.SetDispatcherWithSigData calldata vars) external;
/**
* @notice Sets a profile's URI, which is reflected in the `tokenURI()` function.
* @notice Sets the approval for a delegated executor to act on behalf of the caller.
*
* @param executor The executor to set the approval for.
* @param approved The approval to set.
*/
function setDelegatedExecutorApproval(address executor, bool approved) external;
/**
* @notice Sets the approval for a delegated executor to act on behalf of a given signer.
*
* @param vars A SetDelegatedExecutorApprovalWithSigData struct, including the regular parameters and an EIP712Signature
* struct.
*/
function setDelegatedExecutorApprovalWithSig(
DataTypes.SetDelegatedExecutorApprovalWithSigData calldata vars
) external;
/**
* @notice Sets a profile's image 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.
@@ -162,7 +201,8 @@ interface ILensHub {
function setProfileImageURI(uint256 profileId, string calldata imageURI) external;
/**
* @notice Sets a profile's URI via signature with the specified parameters.
* @notice Sets the image URI via signature for the given profile with the specified parameters. The signer must
* either be the profile owner or a delegated executor.
*
* @param vars A SetProfileImageURIWithSigData struct, including the regular parameters and an EIP712Signature struct.
*/
@@ -178,7 +218,8 @@ interface ILensHub {
function setFollowNFTURI(uint256 profileId, string calldata followNFTURI) external;
/**
* @notice Sets a followNFT URI via signature with the specified parameters.
* @notice Sets a followNFT URI via signature for the given profile with the specified parameters. The signer must
* either be the profile owner or a delegated executor.
*
* @param vars A SetFollowNFTURIWithSigData struct, including the regular parameters and an EIP712Signature struct.
*/
@@ -194,7 +235,8 @@ interface ILensHub {
function post(DataTypes.PostData calldata vars) external returns (uint256);
/**
* @notice Publishes a post to a given profile via signature with the specified parameters.
* @notice Publishes a post to a given profile via signature with the specified parameters. The signer must
* either be the profile owner or a delegated executor.
*
* @param vars A PostWithSigData struct containing the regular parameters and an EIP712Signature struct.
*
@@ -212,7 +254,8 @@ interface ILensHub {
function comment(DataTypes.CommentData calldata vars) external returns (uint256);
/**
* @notice Publishes a comment to a given profile via signature with the specified parameters.
* @notice Publishes a comment to a given profile via signature with the specified parameters. The signer must
* either be the profile owner or a delegated executor.
*
* @param vars A CommentWithSigData struct containing the regular parameters and an EIP712Signature struct.
*
@@ -230,7 +273,8 @@ interface ILensHub {
function mirror(DataTypes.MirrorData calldata vars) external returns (uint256);
/**
* @notice Publishes a mirror to a given profile via signature with the specified parameters.
* @notice Publishes a mirror to a given profile via signature with the specified parameters. The signer must
* either be the profile owner or a delegated executor.
*
* @param vars A MirrorWithSigData struct containing the regular parameters and an EIP712Signature struct.
*
@@ -239,48 +283,101 @@ interface ILensHub {
function mirrorWithSig(DataTypes.MirrorWithSigData calldata vars) external returns (uint256);
/**
* @notice Follows the given profiles, executing each profile's follow module logic (if any) and minting followNFTs to the caller.
* @notice Follows the given profiles, executing each profile's follow module logic (if any).
*
* NOTE: Both the `profileIds` and `datas` arrays must be of the same length, regardless if the profiles do not have a follow module set.
* @dev Both the `idsOfProfilesToFollow`, `followTokenIds`, 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 followerProfileId The ID of the profile the follows are being executed for.
* @param idsOfProfilesToFollow The array of IDs of profiles to follow.
* @param followTokenIds The array of follow token IDs to use for each follow.
* @param datas The arbitrary data array to pass to the follow module for each profile if needed.
*
* @return uint256[] An array of integers representing the minted follow NFTs token IDs.
* @return uint256[] An array follow token IDs used for each follow operation.
*/
function follow(uint256[] calldata profileIds, bytes[] calldata datas)
external
returns (uint256[] memory);
function follow(
uint256 followerProfileId,
uint256[] calldata idsOfProfilesToFollow,
uint256[] calldata followTokenIds,
bytes[] calldata datas
) external returns (uint256[] memory);
/**
* @notice Follows a given profile via signature with the specified parameters.
* @notice Follows the given profiles via signature with the specified parameters. The signer must either be the
* follower or a delegated executor.
*
* @param vars A FollowWithSigData struct containing the regular parameters as well as the signing follower's address
* and an EIP712Signature struct.
* @param vars A FollowWithSigData struct containing the regular parameters as well as the signing follower's
* address and an EIP712Signature struct.
*
* @return uint256[] An array of integers representing the minted follow NFTs token IDs.
* @return uint256[] An array follow token IDs used for each follow operation.
*/
function followWithSig(DataTypes.FollowWithSigData calldata vars)
external
returns (uint256[] memory);
/**
* @notice Unfollows the given profiles.
*
* @param unfollowerProfileId The ID of the profile the unfollows are being executed for.
* @param idsOfProfilesToUnfollow The array of IDs of profiles to unfollow.
*/
function unfollow(uint256 unfollowerProfileId, uint256[] calldata idsOfProfilesToUnfollow)
external;
/**
* @notice Unfollows the given profiles via signature with the specified parameters. The signer must either be the
* unfollower or a delegated executor.
*
* @param vars An UnollowWithSigData struct containing the regular parameters as well as the signing unfollower's
* address and an EIP712Signature struct.
*/
function unfollowWithSig(DataTypes.UnfollowWithSigData calldata vars) external;
/**
* @notice Sets the block status for the given profiles. Changing a profile's block status to `true` (i.e. blocked),
* when it was following, will make it unfollow.
*
* @dev Both the `idsOfProfilesToSetBlockStatus` and `blockStatus` arrays must be of the same length.
*
* @param byProfileId The ID of the profile the block status sets are being executed for.
* @param idsOfProfilesToSetBlockStatus The array of IDs of profiles to set block status.
* @param blockStatus The array of block status to use for each setting.
*/
function setBlockStatus(
uint256 byProfileId,
uint256[] calldata idsOfProfilesToSetBlockStatus,
bool[] calldata blockStatus
) external;
/**
* @notice Blocks the given profiles via signature with the specified parameters. The signer must either be the
* blocker or a delegated executor.
*
* @param vars An SetBlockStatusWithSigData struct containing the regular parameters as well as the signing
* blocker's address and an EIP712Signature struct.
*/
function setBlockStatusWithSig(DataTypes.SetBlockStatusWithSigData 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 collectorProfileId The ID of the profile the collect is being executed from.
* @param publisherProfileId 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.
*
* @return uint256 An integer representing the minted token ID.
*/
function collect(
uint256 profileId,
uint256 collectorProfileId,
uint256 publisherProfileId,
uint256 pubId,
bytes calldata data
) external returns (uint256);
/**
* @notice Collects a given publication via signature with the specified parameters.
* @notice Collects a given publication via signature with the specified parameters. The signer must either be the collector
* or a delegated executor.
*
* @param vars A CollectWithSigData struct containing the regular parameters as well as the collector's address and
* an EIP712Signature struct.
@@ -290,8 +387,8 @@ interface ILensHub {
function collectWithSig(DataTypes.CollectWithSigData calldata vars) external returns (uint256);
/**
* @dev Helper function to emit a detailed followNFT transfer event from the hub, to be consumed by frontends to track
* followNFT transfers.
* @dev Helper function to emit a detailed followNFT transfer event from the hub, to be consumed by indexers 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.
@@ -306,8 +403,8 @@ interface ILensHub {
) external;
/**
* @dev Helper function to emit a detailed collectNFT transfer event from the hub, to be consumed by frontends to track
* collectNFT transfers.
* @dev Helper function to emit a detailed collectNFT transfer event from the hub, to be consumed by indexers 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.
@@ -323,10 +420,32 @@ interface ILensHub {
address to
) external;
/**
* @dev Helper function to emit an `Unfollowed` event from the hub, to be consumed by indexers to track unfollows.
*
* @param unfollowerProfileId The ID of the profile that executed the unfollow.
* @param idOfProfileUnfollowed The ID of the profile that was unfollowed.
*/
function emitUnfollowedEvent(uint256 unfollowerProfileId, uint256 idOfProfileUnfollowed)
external;
/// ************************
/// *****VIEW FUNCTIONS*****
/// ************************
/**
* @notice Returns whether or not `followerProfileId` is following `followedProfileId`.
*
* @param followerProfileId The ID of the profile whose following state should be queried.
* @param followedProfileId The ID of the profile whose followed state should be queried.
*
* @return bool True if `followerProfileId` is following `followedProfileId`, false otherwise.
*/
function isFollowing(uint256 followerProfileId, uint256 followedProfileId)
external
view
returns (bool);
/**
* @notice Returns whether or not a profile creator is whitelisted.
*
@@ -336,15 +455,6 @@ interface ILensHub {
*/
function isProfileCreatorWhitelisted(address profileCreator) external view returns (bool);
/**
* @notice Returns default profile for a given wallet address
*
* @param wallet The address to find the default mapping
*
* @return uint256 The default profile id, which will be 0 if not mapped.
*/
function defaultProfile(address wallet) external view returns (uint256);
/**
* @notice Returns whether or not a follow module is whitelisted.
*
@@ -380,23 +490,75 @@ interface ILensHub {
function getGovernance() external view returns (address);
/**
* @notice Returns the dispatcher associated with a profile.
* @notice Returns whether the given delegated executor is approved to act on behalf of the given
* wallet.
*
* @param wallet The wallet to check the delegated executor approval for.
* @param executor The executor to query the delegated executor approval for.
*
* @return bool True if the executor is approved as a delegated executor to act on behalf of the wallet,
* false otherwise.
*/
function isDelegatedExecutorApproved(address wallet, address executor)
external
view
returns (bool);
/**
* @notice Returns whether `profile` is blocked by `byProfile`.
*
* @param profileId The ID of the profile whose blocked status should be queried.
* @param byProfileId The ID of the profile whose blocker status should be queried.
*
* @return bool True if `profileId` is blocked by `byProfileId`, false otherwise.
*/
function isBlocked(uint256 profileId, uint256 byProfileId) external view returns (bool);
/**
* @notice Returns the default profile for a given wallet address
*
* @param wallet The address to find the default mapping
*
* @return uint256 The default profile id, which will be 0 if not mapped.
*/
function getDefaultProfile(address wallet) external view returns (uint256);
/**
* @notice Returns the metadata URI for a given profile
*
* @param profileId The token ID of the profile to query the metadata URI for.
*
* @return string The metadata URI associated with the given profile.
*/
function getProfileMetadataURI(uint256 profileId) external view returns (string memory);
/**
* @notice Returns the dispatcher for a given profile.
*
* @param profileId The token ID of the profile to query the dispatcher for.
*
* @return address The dispatcher address associated with the profile.
* @return address The dispatcher address associated with the given 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.
* @param profileId The token ID of the profile to query the publication count for.
*
* @return uint256 The number of publications associated with the queried profile.
* @return uint256 The number of publications associated with the given profile.
*/
function getPubCount(uint256 profileId) external view returns (uint256);
/**
* @notice Returns the image URI for a given profile
*
* @param profileId The token ID of the profile to query the image URI for.
*
* @return string The image URI associated with the given profile.
*/
function getProfileImageURI(uint256 profileId) external view returns (string memory);
/**
* @notice Returns the followNFT associated with a given profile, if any.
*
@@ -421,7 +583,7 @@ interface ILensHub {
* @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 address The address of the collectNFT associated with the queried publication.
* @return address The address of the collectNFT associated with the given publication.
*/
function getCollectNFT(uint256 profileId, uint256 pubId) external view returns (address);
@@ -454,15 +616,6 @@ interface ILensHub {
*/
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 string 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.
*
@@ -487,15 +640,6 @@ interface ILensHub {
*/
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 uint256 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.
*

View File

@@ -0,0 +1,7 @@
pragma solidity 0.8.15;
import {DataTypes} from '../libraries/DataTypes.sol';
interface ILensMultiState {
function getState() external view returns (DataTypes.ProtocolState);
}

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {DataTypes} from '../libraries/DataTypes.sol';

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
/**
* @title IModuleGlobals

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
/**
* @title IReferenceModule
@@ -9,9 +9,19 @@ pragma solidity 0.8.10;
* @notice This is the standard interface for all Lens-compatible ReferenceModules.
*/
interface IReferenceModule {
// function getModuleVersion() external view returns (uint256);
// function processModuleChange(
// uint256 profileId,
// uint256 pubId,
// bytes calldata data
// ) external;
/**
* @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 executor The owner or an approved delegated executor.
* @param pubId The associated publication's LensHub publication ID.
* @param data Arbitrary data passed from the user to be decoded.
*
@@ -20,6 +30,7 @@ interface IReferenceModule {
*/
function initializeReferenceModule(
uint256 profileId,
address executor,
uint256 pubId,
bytes calldata data
) external returns (bytes memory);
@@ -28,12 +39,14 @@ interface IReferenceModule {
* @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 executor The commenter or an approved delegated executor.
* @param profileIdPointed The profile ID of the profile associated the publication being referenced.
* @param pubIdPointed The publication ID of the publication being referenced.
* @param data Arbitrary data __passed from the commenter!__ to be decoded.
*/
function processComment(
uint256 profileId,
address executor,
uint256 profileIdPointed,
uint256 pubIdPointed,
bytes calldata data
@@ -43,12 +56,14 @@ interface IReferenceModule {
* @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 executor The mirror creator or an approved delegated executor.
* @param profileIdPointed The profile ID of the profile associated the publication being referenced.
* @param pubIdPointed The publication ID of the publication being referenced.
* @param data Arbitrary data __passed from the mirrorer!__ to be decoded.
*/
function processMirror(
uint256 profileId,
address executor,
uint256 profileIdPointed,
uint256 pubIdPointed,
bytes calldata data

View File

@@ -1,12 +1,125 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
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;
uint16 internal constant MAX_PROFILE_IMAGE_URI_LENGTH = 6000;
}
string constant FOLLOW_NFT_NAME_SUFFIX = '-Follower';
string constant FOLLOW_NFT_SYMBOL_SUFFIX = '-Fl';
string constant COLLECT_NFT_NAME_INFIX = '-Collect-';
string constant COLLECT_NFT_SYMBOL_INFIX = '-Cl-';
uint8 constant MAX_HANDLE_LENGTH = 31;
uint16 constant MAX_PROFILE_IMAGE_URI_LENGTH = 6000;
// We store constants equal to the storage slots here to later access via inline
// assembly without needing to pass storage pointers. The NAME_SLOT_GT_31 slot
// is equivalent to keccak256(NAME_SLOT) and is where the name string is stored
// if the length is greater than 31 bytes.
uint256 constant NAME_SLOT = 0;
uint256 constant TOKEN_DATA_MAPPING_SLOT = 2;
uint256 constant TOKEN_APPROVAL_MAPPING_SLOT = 4;
uint256 constant OPERATOR_APPROVAL_MAPPING_SLOT = 5;
uint256 constant SIG_NONCES_MAPPING_SLOT = 10;
uint256 constant PROTOCOL_STATE_SLOT = 12;
uint256 constant PROFILE_CREATOR_WHITELIST_MAPPING_SLOT = 13;
uint256 constant FOLLOW_MODULE_WHITELIST_MAPPING_SLOT = 14;
uint256 constant COLLECT_MODULE_WHITELIST_MAPPING_SLOT = 15;
uint256 constant REFERENCE_MODULE_WHITELIST_MAPPING_SLOT = 16;
uint256 constant DISPATCHER_BY_PROFILE_MAPPING_SLOT = 17;
uint256 constant PROFILE_ID_BY_HANDLE_HASH_MAPPING_SLOT = 18;
uint256 constant PROFILE_BY_ID_MAPPING_SLOT = 19;
uint256 constant PUB_BY_ID_BY_PROFILE_MAPPING_SLOT = 20;
uint256 constant DEFAULT_PROFILE_MAPPING_SLOT = 21;
uint256 constant PROFILE_COUNTER_SLOT = 22;
uint256 constant GOVERNANCE_SLOT = 23;
uint256 constant EMERGENCY_ADMIN_SLOT = 24;
uint256 constant DELEGATED_EXECUTOR_APPROVAL_MAPPING_SLOT = 25;
uint256 constant PROFILE_METADATA_MAPPING_SLOT = 26;
uint256 constant BLOCK_STATUS_MAPPING_SLOT = 27;
uint256 constant NAME_SLOT_GT_31 = 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563;
// We store the polygon chain ID and domain separator as constants to save gas.
uint256 constant POLYGON_CHAIN_ID = 137;
bytes32 constant POLYGON_DOMAIN_SEPARATOR = 0x78e10b2874b1a1d4436464e65903d3bdc28b68f8d023df2e47b65f8caa45c4bb;
// keccak256(
// abi.encode(
// EIP712_DOMAIN_TYPEHASH,
// keccak256('Lens Protocol Profiles'),
// EIP712_REVISION_HASH,
// POLYGON_CHAIN_ID,
// address(0xDb46d1Dc155634FbC732f92E853b10B288AD5a1d)
// )
// );
// Profile struct offsets
// uint256 pubCount; // offset 0
uint256 constant PROFILE_FOLLOW_MODULE_OFFSET = 1;
uint256 constant PROFILE_FOLLOW_NFT_OFFSET = 2;
uint256 constant PROFILE_HANDLE_OFFSET = 3;
uint256 constant PROFILE_IMAGE_URI_OFFSET = 4;
uint256 constant PROFILE_FOLLOW_NFT_URI_OFFSET = 5;
// Publication struct offsets
// uint256 profileIdPointed; // offset 0
uint256 constant PUBLICATION_PUB_ID_POINTED_OFFSET = 1;
uint256 constant PUBLICATION_CONTENT_URI_OFFSET = 2; // offset 2
uint256 constant PUBLICATION_REFERENCE_MODULE_OFFSET = 3; // offset 3
uint256 constant PUBLICATION_COLLECT_MODULE_OFFSET = 4; // offset 4
uint256 constant PUBLICATION_COLLECT_NFT_OFFSET = 5; // offset 4
// We also store typehashes here
bytes32 constant EIP712_REVISION_HASH = keccak256('1');
bytes32 constant PERMIT_TYPEHASH = keccak256(
'Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)'
);
bytes32 constant PERMIT_FOR_ALL_TYPEHASH = keccak256(
'PermitForAll(address owner,address operator,bool approved,uint256 nonce,uint256 deadline)'
);
bytes32 constant BURN_WITH_SIG_TYPEHASH = keccak256(
'BurnWithSig(uint256 tokenId,uint256 nonce,uint256 deadline)'
);
bytes32 constant EIP712_DOMAIN_TYPEHASH = keccak256(
'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'
);
bytes32 constant SET_DEFAULT_PROFILE_WITH_SIG_TYPEHASH = keccak256(
'SetDefaultProfileWithSig(address wallet,uint256 profileId,uint256 nonce,uint256 deadline)'
);
bytes32 constant SET_FOLLOW_MODULE_WITH_SIG_TYPEHASH = keccak256(
'SetFollowModuleWithSig(uint256 profileId,address followModule,bytes followModuleInitData,uint256 nonce,uint256 deadline)'
);
bytes32 constant SET_FOLLOW_NFT_URI_WITH_SIG_TYPEHASH = keccak256(
'SetFollowNFTURIWithSig(uint256 profileId,string followNFTURI,uint256 nonce,uint256 deadline)'
);
bytes32 constant SET_DISPATCHER_WITH_SIG_TYPEHASH = keccak256(
'SetDispatcherWithSig(uint256 profileId,address dispatcher,uint256 nonce,uint256 deadline)'
);
bytes32 constant SET_DELEGATED_EXECUTOR_APPROVAL_WITH_SIG_TYPEHASH = keccak256(
'SetDelegatedExecutorApprovalWithSig(address executor,bool approved,uint256 nonce,uint256 deadline)'
);
bytes32 constant SET_PROFILE_IMAGE_URI_WITH_SIG_TYPEHASH = keccak256(
'SetProfileImageURIWithSig(uint256 profileId,string imageURI,uint256 nonce,uint256 deadline)'
);
bytes32 constant POST_WITH_SIG_TYPEHASH = keccak256(
'PostWithSig(uint256 profileId,string contentURI,address collectModule,bytes collectModuleInitData,address referenceModule,bytes referenceModuleInitData,uint256 nonce,uint256 deadline)'
);
bytes32 constant COMMENT_WITH_SIG_TYPEHASH = keccak256(
'CommentWithSig(uint256 profileId,string contentURI,uint256 profileIdPointed,uint256 pubIdPointed,bytes referenceModuleData,address collectModule,bytes collectModuleInitData,address referenceModule,bytes referenceModuleInitData,uint256 nonce,uint256 deadline)'
);
bytes32 constant MIRROR_WITH_SIG_TYPEHASH = keccak256(
'MirrorWithSig(uint256 profileId,uint256 profileIdPointed,uint256 pubIdPointed,bytes referenceModuleData,address referenceModule,bytes referenceModuleInitData,uint256 nonce,uint256 deadline)'
);
bytes32 constant FOLLOW_WITH_SIG_TYPEHASH = keccak256(
'FollowWithSig(uint256 followerProfileId,uint256[] idsOfProfilesToFollow,uint256[] followTokenIds,bytes[] datas,uint256 nonce,uint256 deadline)'
);
bytes32 constant UNFOLLOW_WITH_SIG_TYPEHASH = keccak256(
'UnfollowWithSig(uint256 unfollowerProfileId,uint256[] idsOfProfilesToUnfollow,uint256 nonce,uint256 deadline)'
);
bytes32 constant SET_BLOCK_STATUS_WITH_SIG_TYPEHASH = keccak256(
'SetBlockStatusWithSig(uint256 byProfileId,uint256[] idsOfProfilesToSetBlockStatus,bool[] blockStatus,uint256 nonce,uint256 deadline)'
);
bytes32 constant COLLECT_WITH_SIG_TYPEHASH = keccak256(
'CollectWithSig(uint256 collectorProfileId,uint256 publisherProfileId,uint256 pubId,bytes data,uint256 nonce,uint256 deadline)'
);
bytes32 constant SET_PROFILE_METADATA_URI_WITH_SIG_TYPEHASH = keccak256(
'SetProfileMetadataURIWithSig(uint256 profileId,string metadata,uint256 nonce,uint256 deadline)'
);
bytes4 constant EIP1271_MAGIC_VALUE = 0x1626ba7e;

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
/**
* @title DataTypes
@@ -58,17 +58,17 @@ library DataTypes {
* @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 handleDeprecated The deprecated handle slot, no longer used. .
* @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;
uint256 pubCount; // offset 0
address followModule; // offset 1
address followNFT; // offset 2
string handleDeprecated; // offset 3
string imageURI; // offset 4
string followNFTURI; // offset 5
}
/**
@@ -94,7 +94,6 @@ library DataTypes {
* @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 followModuleInitData The follow module initialization data, if any.
@@ -102,7 +101,6 @@ library DataTypes {
*/
struct CreateProfileData {
address to;
string handle;
string imageURI;
address followModule;
bytes followModuleInitData;
@@ -113,11 +111,13 @@ library DataTypes {
* @notice A struct containing the parameters required for the `setDefaultProfileWithSig()` function. Parameters are
* the same as the regular `setDefaultProfile()` function, with an added EIP712Signature.
*
* @param delegatedSigner The delegated executor signer, must be either zero, defaulting to the wallet owner, or a delegated executor.
* @param wallet The address of the wallet setting the default profile.
* @param profileId The token ID of the profile which will be set as default, or zero.
* @param sig The EIP712Signature struct containing the profile owner's signature.
*/
struct SetDefaultProfileWithSigData {
address delegatedSigner;
address wallet;
uint256 profileId;
EIP712Signature sig;
@@ -127,12 +127,14 @@ library DataTypes {
* @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 delegatedSigner The delegated executor signer, must be either zero, defaulting to the profile owner, or a delegated executor.
* @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 followModuleInitData The data to be passed to the followModule for initialization.
* @param sig The EIP712Signature struct containing the profile owner's signature.
*/
struct SetFollowModuleWithSigData {
address delegatedSigner;
uint256 profileId;
address followModule;
bytes followModuleInitData;
@@ -141,7 +143,7 @@ library DataTypes {
/**
* @notice A struct containing the parameters required for the `setDispatcherWithSig()` function. Parameters are the same
* as the regular `setDispatcher()` function, with an added EIP712Signature.
* as the regular `setDispatcher()` function, with an added EIP712Signature. The signer must be the owner.
*
* @param profileId The token ID of the profile to set the dispatcher for.
* @param dispatcher The dispatcher address to set for the profile.
@@ -153,15 +155,33 @@ library DataTypes {
EIP712Signature sig;
}
/**
* @notice A struct containing the parameters required for the `setDelegatedExecutorApprovalWithSig()` function. Parameters
* are the same as the regular `setDelegatedExecutorApproval()` function. The signer must be the onBehalfOf address.
*
* @param onBehalfOf The address the delegated executor is to be granted or revoked approval to act on behalf of.
* @param executor The executor to set the approval for.
* @param approved Whether the executor is to be approved.
* @param sig The EIP712Signature struct containing to the signer setting the approval's signature.
*/
struct SetDelegatedExecutorApprovalWithSigData {
address onBehalfOf;
address executor;
bool approved;
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 delegatedSigner The delegated executor signer, must be either zero, defaulting to the profile owner, or a delegated executor.
* @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 {
address delegatedSigner;
uint256 profileId;
string imageURI;
EIP712Signature sig;
@@ -171,11 +191,13 @@ library DataTypes {
* @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 delegatedSigner The delegated executor signer, must be either zero, defaulting to the profile owner, or a delegated executor.
* @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 {
address delegatedSigner;
uint256 profileId;
string followNFTURI;
EIP712Signature sig;
@@ -204,6 +226,7 @@ library DataTypes {
* @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 delegatedSigner The delegated executor signer, must be either zero, defaulting to the profile owner, or a delegated executor.
* @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.
@@ -213,6 +236,7 @@ library DataTypes {
* @param sig The EIP712Signature struct containing the profile owner's signature.
*/
struct PostWithSigData {
address delegatedSigner;
uint256 profileId;
string contentURI;
address collectModule;
@@ -251,6 +275,7 @@ library DataTypes {
* @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 delegatedSigner The delegated executor signer, must be either zero, defaulting to the profile owner, or a delegated executor.
* @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.
@@ -263,6 +288,7 @@ library DataTypes {
* @param sig The EIP712Signature struct containing the profile owner's signature.
*/
struct CommentWithSigData {
address delegatedSigner;
uint256 profileId;
string contentURI;
uint256 profileIdPointed;
@@ -298,6 +324,7 @@ library DataTypes {
* @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 delegatedSigner The delegated executor signer, must be either zero, defaulting to the profile owner, or a delegated executor.
* @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.
@@ -307,6 +334,7 @@ library DataTypes {
* @param sig The EIP712Signature struct containing the profile owner's signature.
*/
struct MirrorWithSigData {
address delegatedSigner;
uint256 profileId;
uint256 profileIdPointed;
uint256 pubIdPointed;
@@ -317,34 +345,93 @@ library DataTypes {
}
/**
* @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.
* @notice A struct containing the parameters required for the `followWithSig` function. Parameters are the same
* as the regular `follow` function, with the 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 delegatedSigner The delegated executor signer, must be either zero, defaulting to the follower,
* or a delegated executor.
* @param followerProfileId The ID of the profile the follows are being executed for.
* @param idsOfProfilesToFollow The array of IDs of profiles to follow.
* @param followTokenIds The array of follow token IDs to use for each follow.
* @param datas The arbitrary data array to pass to the follow module for each profile if needed.
* @param sig The EIP712Signature struct containing the follower's signature.
*/
struct FollowWithSigData {
address follower;
uint256[] profileIds;
address delegatedSigner;
uint256 followerProfileId;
uint256[] idsOfProfilesToFollow;
uint256[] followTokenIds;
bytes[] datas;
EIP712Signature sig;
}
/**
* @notice A struct containing the parameters required for the `unfollowWithSig` function. Parameters are the same
* as the regular `unfollow` function, with the signer address and an EIP712Signature added.
*
* @param delegatedSigner The delegated executor signer, must be either zero, defaulting to the follower,
* or a delegated executor.
* @param unfollowerProfileId The ID of the profile the unfollows are being executed for.
* @param idsOfProfilesToUnfollow The array of IDs of profiles to unfollow.
* @param sig The EIP712Signature struct containing the follower's signature.
*/
struct UnfollowWithSigData {
address delegatedSigner;
uint256 unfollowerProfileId;
uint256[] idsOfProfilesToUnfollow;
EIP712Signature sig;
}
/**
* @notice A struct containing the parameters required for the `setBlockStatusWithSig` function. Parameters are the
* same as the regular `setBlockStatus` function, with the signer address and an EIP712Signature added.
*
* @param delegatedSigner The delegated executor signer, must be either zero, defaulting to the blocker,
* or a delegated executor.
* @param byProfileId The ID of the profile the block status sets are being executed for.
* @param idsOfProfilesToSetBlockStatus The array of IDs of profiles to set block status.
* @param blockStatus The array of block status to use for each setting.
* @param sig The EIP712Signature struct containing the blocker's signature.
*/
struct SetBlockStatusWithSigData {
address delegatedSigner;
uint256 byProfileId;
uint256[] idsOfProfilesToSetBlockStatus;
bool[] blockStatus;
EIP712Signature sig;
}
/**
* @notice A struct containing the parameters required for the `collect()` function.
*
* @param collector The address of the collector.
* @param profileId The token ID of the profile to that published the content being collected.
* @param pubId The ID of the publication being collected.
* @param data The data passed to the collect module.
*/
struct CollectData {
// TODO: Move this to tests? And the one below
uint256 collectorProfileId;
uint256 publisherProfileId;
uint256 pubId;
bytes data;
}
/**
* @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 delegatedSigner The delegated executor signer, must be either zero, defaulting to the collector profile owner, or a delegated executor.
* @param collectorProfileId The collector profile.
* @param publisherProfileId 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;
address delegatedSigner;
uint256 collectorProfileId;
uint256 publisherProfileId;
uint256 pubId;
bytes data;
EIP712Signature sig;
@@ -353,19 +440,23 @@ library DataTypes {
/**
* @notice A struct containing the parameters required for the `setProfileMetadataWithSig()` function.
*
* @param delegatedSigner The delegated executor signer, must be either zero, defaulting to the profile owner, or a delegated executor.
* @param profileId The profile ID for which to set the metadata.
* @param metadata The metadata string to set for the profile and user.
* @param metadataURI The metadata string to set for the profile and user.
* @param sig The EIP712Signature struct containing the user's signature.
*/
struct SetProfileMetadataWithSigData {
struct SetProfileMetadataURIWithSigData {
address delegatedSigner;
uint256 profileId;
string metadata;
string metadataURI;
EIP712Signature sig;
}
/**
* @notice A struct containing the parameters required for the `toggleFollowWithSig()` function.
*
* @note This does not include a delegatedSigner parameter as it is marked for deprecation.
*
* @param follower The follower which is the message signer.
* @param profileIds The token ID array of the profiles.
* @param enables The array of booleans to enable/disable follows.

View File

@@ -1,8 +1,31 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
library Errors {
// ERC721Time Errors
error ERC721Time_BalanceQueryForZeroAddress();
error ERC721Time_OwnerQueryForNonexistantToken();
error ERC721Time_MintTimestampQueryForNonexistantToken();
error ERC721Time_TokenDataQueryForNonexistantToken();
error ERC721Time_URIQueryForNonexistantToken();
error ERC721Time_ApprovalToCurrentOwner();
error ERC721Time_ApproveCallerNotOwnerOrApprovedForAll();
error ERC721Time_ApprovedQueryForNonexistantToken();
error ERC721Time_ApproveToCaller();
error ERC721Time_TransferCallerNotOwnerOrApproved();
error ERC721Time_TransferToNonERC721ReceiverImplementer();
error ERC721Time_OperatorQueryForNonexistantToken();
error ERC721Time_MintToZeroAddress();
error ERC721Time_TokenAlreadyMinted();
error ERC721Time_TransferOfTokenThatIsNotOwn();
error ERC721Time_TransferToZeroAddress();
// ERC721Enumerable Errors
error ERC721Enumerable_OwnerIndexOutOfBounds();
error ERC721Enumerable_GlobalIndexOutOfBounds();
// Lens Protocol Errors
error CannotInitImplementation();
error Initialized();
error SignatureExpired();
@@ -13,14 +36,14 @@ library Errors {
error TokenDoesNotExist();
error NotGovernance();
error NotGovernanceOrEmergencyAdmin();
error EmergencyAdminCannotUnpause();
error EmergencyAdminCanOnlyPauseFurther();
error CallerNotWhitelistedModule();
error CollectModuleNotWhitelisted();
error FollowModuleNotWhitelisted();
error ReferenceModuleNotWhitelisted();
error ProfileCreatorNotWhitelisted();
error NotProfileOwner();
error NotProfileOwnerOrDispatcher();
error NotProfileOwnerOrValid(); // deprecated
error NotDispatcher();
error PublicationDoesNotExist();
error HandleTaken();
@@ -35,6 +58,11 @@ library Errors {
error CannotCommentOnSelf();
error NotWhitelisted();
error InvalidParameter();
error ExecutorInvalid();
error Blocked();
error SelfBlock();
error NotFollowing();
error SelfFollow();
// Module Errors
error InitParamsInvalid();

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {DataTypes} from './DataTypes.sol';
@@ -119,7 +119,6 @@ library Events {
* @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
@@ -131,7 +130,6 @@ library Events {
uint256 indexed profileId,
address indexed creator,
address indexed to,
string handle,
string imageURI,
address followModule,
bytes followModuleReturnData,
@@ -157,6 +155,19 @@ library Events {
*/
event DispatcherSet(uint256 indexed profileId, address indexed dispatcher, uint256 timestamp);
/**
* @dev Emitted when a delegated executor is granted or revoked approval to act on behalf of a given address.
*
* @param onBehalfOf The address the delegated executor is granted or revoked approval to act on behalf of.
* @param executor The address of the delegated executor granted or revoked approval.
* @param approved Whether the executor is approved.
*/
event DelegatedExecutorApprovalSet(
address indexed onBehalfOf,
address indexed executor,
bool indexed approved
);
/**
* @dev Emitted when a profile's URI is set.
*
@@ -302,8 +313,8 @@ library Events {
/**
* @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 collectorProfileId The address collecting the publication.
* @param publisherProfileId 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.
@@ -311,8 +322,8 @@ library Events {
* @param timestamp The current block timestamp.
*/
event Collected(
address indexed collector,
uint256 indexed profileId,
uint256 indexed collectorProfileId,
uint256 indexed publisherProfileId,
uint256 indexed pubId,
uint256 rootProfileId,
uint256 rootPubId,
@@ -321,20 +332,53 @@ library Events {
);
/**
* @dev Emitted upon a successful follow action.
* @dev Emitted upon a successful follow operation.
*
* @param follower The address following the given profiles.
* @param profileIds The token ID array of the profiles being followed.
* @param followModuleDatas The array of data parameters passed to each follow module.
* @param timestamp The current block timestamp.
* @param followerProfileId The ID of the profile that executed the follow.
* @param idOfProfileFollowed The ID of the profile that was followed.
* @param followTokenIdAssigned The ID of the follow token assigned to the follower.
* @param followModuleData The data to passed to the follow module, if any.
* @param timestamp The timestamp of the follow operation.
*/
event Followed(
address indexed follower,
uint256[] profileIds,
bytes[] followModuleDatas,
uint256 indexed followerProfileId,
uint256 idOfProfileFollowed,
uint256 followTokenIdAssigned,
bytes followModuleData,
uint256 timestamp
);
/**
* @dev Emitted upon a successful unfollow operation.
*
* @param unfollowerProfileId The ID of the profile that executed the unfollow.
* @param idOfProfileUnfollowed The ID of the profile that was unfollowed.
* @param timestamp The timestamp of the unfollow operation.
*/
event Unfollowed(
uint256 indexed unfollowerProfileId,
uint256 idOfProfileUnfollowed,
uint256 timestamp
);
/**
* @dev Emitted upon a successful block, through a block status setting operation.
*
* @param byProfileId The ID of the profile that executed the block status change.
* @param idOfProfileBlocked The ID of the profile whose block status have been set to blocked.
* @param timestamp The timestamp of the block operation.
*/
event Blocked(uint256 indexed byProfileId, uint256 idOfProfileBlocked, uint256 timestamp);
/**
* @dev Emitted upon a successful unblock, through a block status setting operation.
*
* @param byProfileId The ID of the profile that executed the block status change.
* @param idOfProfileUnblocked The ID of the profile whose block status have been set to unblocked.
* @param timestamp The timestamp of the unblock operation.
*/
event Unblocked(uint256 indexed byProfileId, uint256 idOfProfileUnblocked, uint256 timestamp);
/**
* @dev Emitted via callback when a followNFT is transferred.
*

View File

@@ -0,0 +1,485 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {GeneralHelpers} from './helpers/GeneralHelpers.sol';
import {MetaTxHelpers} from './helpers/MetaTxHelpers.sol';
import {InteractionHelpers} from './helpers/InteractionHelpers.sol';
import {DataTypes} from './DataTypes.sol';
import {Errors} from './Errors.sol';
import {Events} from './Events.sol';
import {IFollowModule} from '../interfaces/IFollowModule.sol';
import {ICollectModule} from '../interfaces/ICollectModule.sol';
import {IReferenceModule} from '../interfaces/IReferenceModule.sol';
import {IDeprecatedFollowModule} from '../interfaces/IDeprecatedFollowModule.sol';
import {IDeprecatedCollectModule} from '../interfaces/IDeprecatedCollectModule.sol';
import {IDeprecatedReferenceModule} from '../interfaces/IDeprecatedReferenceModule.sol';
import './Constants.sol';
/**
* @title GeneralLib
* @author Lens Protocol
*
* @notice This is the library that contains logic to be called by the hub via `delegateCall`.
*
* Note: The setDispatcher non-signature function was not migrated as it was more space-efficient
* to leave it in the hub.
*/
library GeneralLib {
/**
* @notice Sets the governance address.
*
* @param newGovernance The new governance address to set.
*/
function setGovernance(address newGovernance) external {
address prevGovernance;
assembly {
prevGovernance := sload(GOVERNANCE_SLOT)
sstore(GOVERNANCE_SLOT, newGovernance)
}
emit Events.GovernanceSet(msg.sender, prevGovernance, newGovernance, block.timestamp);
}
/**
* @notice Sets the emergency admin address.
*
* @param newEmergencyAdmin The new governance address to set.
*/
function setEmergencyAdmin(address newEmergencyAdmin) external {
address prevEmergencyAdmin;
assembly {
prevEmergencyAdmin := sload(EMERGENCY_ADMIN_SLOT)
sstore(EMERGENCY_ADMIN_SLOT, newEmergencyAdmin)
}
emit Events.EmergencyAdminSet(
msg.sender,
prevEmergencyAdmin,
newEmergencyAdmin,
block.timestamp
);
}
/**
* @notice Sets the protocol state, only meant to be called at initialization since
* this does nto validate the caller.
*
* @param newState The new protocol state to set.
*/
function initState(DataTypes.ProtocolState newState) external {
DataTypes.ProtocolState prevState;
assembly {
prevState := sload(PROTOCOL_STATE_SLOT)
sstore(PROTOCOL_STATE_SLOT, newState)
}
emit Events.StateSet(msg.sender, prevState, newState, block.timestamp);
}
/**
* @notice Sets the protocol state and validates the caller. The emergency admin can only
* pause further (Unpaused => PublishingPaused => Paused). Whereas governance can set any
* state.
*
* @param newState The new protocol state to set.
*/
function setState(DataTypes.ProtocolState newState) external {
address emergencyAdmin;
address governance;
DataTypes.ProtocolState prevState;
// Load the emergency admin, governance and protocol state, then store the new protocol
// state via assembly.
assembly {
emergencyAdmin := sload(EMERGENCY_ADMIN_SLOT)
governance := sload(GOVERNANCE_SLOT)
prevState := sload(PROTOCOL_STATE_SLOT)
sstore(PROTOCOL_STATE_SLOT, newState)
}
// If the sender is the emergency admin, prevent them from reducing restrictions.
if (msg.sender == emergencyAdmin) {
if (newState <= prevState) revert Errors.EmergencyAdminCanOnlyPauseFurther();
} else if (msg.sender != governance) {
revert Errors.NotGovernanceOrEmergencyAdmin();
}
emit Events.StateSet(msg.sender, prevState, newState, block.timestamp);
}
/**
* @notice Sets the default profile for a given wallet.
*
* @param onBehalfOf The wallet to set the default profile for.
* @param profileId The profile ID to set.
*/
function setDefaultProfile(address onBehalfOf, uint256 profileId) external {
_validateCallerIsOnBehalfOfOrExecutor(onBehalfOf);
_setDefaultProfile(onBehalfOf, profileId);
}
/**
* @notice Sets the default profile via signature for a given owner.
*
* @param vars the SetDefaultProfileWithSigData struct containing the relevant parameters.
*/
function setDefaultProfileWithSig(DataTypes.SetDefaultProfileWithSigData calldata vars)
external
{
uint256 profileId = vars.profileId;
address wallet = vars.wallet;
address signer = GeneralHelpers.getOriginatorOrDelegatedExecutorSigner(
wallet,
vars.delegatedSigner
);
MetaTxHelpers.baseSetDefaultProfileWithSig(signer, vars);
_setDefaultProfile(wallet, profileId);
}
function setDelegatedExecutorApproval(address executor, bool approved) external {
_setDelegatedExecutorApproval(msg.sender, executor, approved);
}
function setDelegatedExecutorApprovalWithSig(
DataTypes.SetDelegatedExecutorApprovalWithSigData calldata vars
) external {
MetaTxHelpers.baseSetDelegatedExecutorApprovalWithSig(vars);
_setDelegatedExecutorApproval(vars.onBehalfOf, vars.executor, vars.approved);
}
/**
* @notice Follows the given profiles, executing the necessary logic and module calls before minting the follow
* NFT(s) to the follower.
*
* @param followerProfileId The profile the follow is being executed for.
* @param idsOfProfilesToFollow The array of profile token IDs to follow.
* @param followTokenIds The array of follow token IDs to use for each follow.
* @param followModuleDatas The array of follow module data parameters to pass to each profile's follow module.
*
* @return uint256[] An array of integers representing the minted follow NFTs token IDs.
*/
function follow(
uint256 followerProfileId,
uint256[] calldata idsOfProfilesToFollow,
uint256[] calldata followTokenIds,
bytes[] calldata followModuleDatas
) external returns (uint256[] memory) {
GeneralHelpers.validateCallerIsOwnerOrDelegatedExecutor(followerProfileId);
return
InteractionHelpers.follow({
followerProfileId: followerProfileId,
executor: msg.sender,
idsOfProfilesToFollow: idsOfProfilesToFollow,
followTokenIds: followTokenIds,
followModuleDatas: followModuleDatas
});
}
/**
* @notice Validates parameters and increments the nonce for a given owner using the
* `followWithSig()` function.
*
* @param vars the FollowWithSigData struct containing the relevant parameters.
*/
function followWithSig(DataTypes.FollowWithSigData calldata vars)
external
returns (uint256[] memory)
{
address followerProfileOwner = GeneralHelpers.ownerOf(vars.followerProfileId);
address signer = GeneralHelpers.getOriginatorOrDelegatedExecutorSigner(
followerProfileOwner,
vars.delegatedSigner
);
MetaTxHelpers.baseFollowWithSig(signer, vars);
return
InteractionHelpers.follow({
followerProfileId: vars.followerProfileId,
executor: signer,
idsOfProfilesToFollow: vars.idsOfProfilesToFollow,
followTokenIds: vars.followTokenIds,
followModuleDatas: vars.datas
});
}
function unfollow(uint256 unfollowerProfileId, uint256[] calldata idsOfProfilesToUnfollow)
external
{
GeneralHelpers.validateCallerIsOwnerOrDelegatedExecutor(unfollowerProfileId);
return
InteractionHelpers.unfollow({
unfollowerProfileId: unfollowerProfileId,
executor: msg.sender,
idsOfProfilesToUnfollow: idsOfProfilesToUnfollow
});
}
/**
* @notice Validates parameters and increments the nonce for a given owner using the
* `unfollowWithSig()` function.
*
* @param vars the UnfollowWithSigData struct containing the relevant parameters.
*/
function unfollowWithSig(DataTypes.UnfollowWithSigData calldata vars) external {
address unfollowerProfileOwner = GeneralHelpers.ownerOf(vars.unfollowerProfileId);
address signer = GeneralHelpers.getOriginatorOrDelegatedExecutorSigner(
unfollowerProfileOwner,
vars.delegatedSigner
);
MetaTxHelpers.baseUnfollowWithSig(signer, vars);
return
InteractionHelpers.unfollow({
unfollowerProfileId: vars.unfollowerProfileId,
executor: signer,
idsOfProfilesToUnfollow: vars.idsOfProfilesToUnfollow
});
}
function setBlockStatus(
uint256 byProfileId,
uint256[] calldata idsOfProfilesToSetBlockStatus,
bool[] calldata blockStatus
) external {
GeneralHelpers.validateCallerIsOwnerOrDelegatedExecutor(byProfileId);
InteractionHelpers.setBlockStatus(byProfileId, idsOfProfilesToSetBlockStatus, blockStatus);
}
function setBlockStatusWithSig(DataTypes.SetBlockStatusWithSigData calldata vars) external {
address blockerProfileOwner = GeneralHelpers.ownerOf(vars.byProfileId);
address signer = GeneralHelpers.getOriginatorOrDelegatedExecutorSigner(
blockerProfileOwner,
vars.delegatedSigner
);
MetaTxHelpers.baseSetBlockStatusWithSig(signer, vars);
InteractionHelpers.setBlockStatus(
vars.byProfileId,
vars.idsOfProfilesToSetBlockStatus,
vars.blockStatus
);
}
/**
* @notice Collects the given publication, executing the necessary logic and module call before minting the
* collect NFT to the collector.
*
* @param collectorProfileId The profile that collect is being executed for.
* @param publisherProfileId The token ID of the publisher profile of the collected publication.
* @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.
*
* @return uint256 An integer representing the minted token ID.
*/
function collect(
uint256 collectorProfileId,
uint256 publisherProfileId,
uint256 pubId,
bytes calldata collectModuleData,
address collectNFTImpl
) external returns (uint256) {
return
InteractionHelpers.collect({
collectorProfileId: collectorProfileId,
collectorProfileOwner: GeneralHelpers.ownerOf(collectorProfileId),
transactionExecutor: msg.sender,
publisherProfileId: publisherProfileId,
pubId: pubId,
collectModuleData: collectModuleData,
collectNFTImpl: collectNFTImpl
});
}
/**
* @notice Validates parameters and increments the nonce for a given owner using the
* `collectWithSig()` function.
*
* @param vars the CollectWithSigData struct containing the relevant parameters.
*/
function collectWithSig(DataTypes.CollectWithSigData calldata vars, address collectNFTImpl)
external
returns (uint256)
{
address collectorProfileOwner = GeneralHelpers.ownerOf(vars.collectorProfileId);
address transactionSigner = GeneralHelpers.getOriginatorOrDelegatedExecutorSigner(
collectorProfileOwner,
vars.delegatedSigner
);
MetaTxHelpers.baseCollectWithSig(transactionSigner, vars);
return
InteractionHelpers.collect({
collectorProfileId: vars.collectorProfileId,
collectorProfileOwner: collectorProfileOwner,
transactionExecutor: transactionSigner,
publisherProfileId: vars.publisherProfileId,
pubId: vars.pubId,
collectModuleData: vars.data,
collectNFTImpl: collectNFTImpl
});
}
/**
* @notice Approves an address to spend a token using via signature.
*
* @param spender The spender to approve.
* @param tokenId The token ID to approve the spender for.
* @param sig the EIP712Signature struct containing the token owner's signature.
*/
function permit(
address spender,
uint256 tokenId,
DataTypes.EIP712Signature calldata sig
) external {
// The `Approved()` event is emitted from `basePermit()`.
MetaTxHelpers.basePermit(spender, tokenId, sig);
// Store the approved address in the token's approval mapping slot.
assembly {
mstore(0, tokenId)
mstore(32, TOKEN_APPROVAL_MAPPING_SLOT)
let slot := keccak256(0, 64)
sstore(slot, spender)
}
}
/**
* @notice Approves a user to operate on all of an owner's tokens via signature.
*
* @param owner The owner to approve the operator for, this is the signer.
* @param operator The operator to approve for the owner.
* @param approved Whether or not the operator should be approved.
* @param sig the EIP712Signature struct containing the token owner's signature.
*/
function permitForAll(
address owner,
address operator,
bool approved,
DataTypes.EIP712Signature calldata sig
) external {
// The `ApprovedForAll()` event is emitted from `basePermitForAll()`.
MetaTxHelpers.basePermitForAll(owner, operator, approved, sig);
// Store whether the operator is approved in the appropriate mapping slot.
assembly {
mstore(0, owner)
mstore(32, OPERATOR_APPROVAL_MAPPING_SLOT)
mstore(32, keccak256(0, 64))
mstore(0, operator)
let slot := keccak256(0, 64)
sstore(slot, approved)
}
}
/**
* @notice Validates parameters and increments the nonce for a given owner using the
* `burnWithSig()` function.
*
* @param tokenId The token ID to burn.
* @param sig the EIP712Signature struct containing the token owner's signature.
*/
function baseBurnWithSig(uint256 tokenId, DataTypes.EIP712Signature calldata sig) external {
MetaTxHelpers.baseBurnWithSig(tokenId, sig);
}
/**
* @notice Returns the domain separator.
*
* @return bytes32 The domain separator.
*/
function getDomainSeparator() external view returns (bytes32) {
return MetaTxHelpers.getDomainSeparator();
}
function getContentURI(uint256 profileId, uint256 pubId) external view returns (string memory) {
(uint256 rootProfileId, uint256 rootPubId) = GeneralHelpers.getPointedIfMirror(
profileId,
pubId
);
string memory ptr;
assembly {
// Load the free memory pointer, where we'll return the value
ptr := mload(64)
// Load the slot, which either contains the content URI + 2*length if length < 32 or
// 2*length+1 if length >= 32, and the actual string starts at slot keccak256(slot)
mstore(0, rootProfileId)
mstore(32, PUB_BY_ID_BY_PROFILE_MAPPING_SLOT)
mstore(32, keccak256(0, 64))
mstore(0, rootPubId)
let slot := add(keccak256(0, 64), PUBLICATION_CONTENT_URI_OFFSET)
let slotLoad := sload(slot)
let size
// Determine if the length > 32 by checking the lowest order bit, meaning the string
// itself is stored at keccak256(slot)
switch and(slotLoad, 1)
case 0 {
// The content URI is in the same slot
// Determine the size by dividing the last byte's value by 2
size := shr(1, and(slotLoad, 255))
// Store the size in the first slot
mstore(ptr, size)
// Store the actual string in the second slot (without the size)
mstore(add(ptr, 32), and(slotLoad, not(255)))
}
case 1 {
// The content URI is not in the same slot
// Determine the size by dividing the value in the whole slot minus 1 by 2
size := shr(1, sub(slotLoad, 1))
// Store the size in the first slot
mstore(ptr, size)
// Compute the total memory slots we need, this is (size + 31) / 32
let totalMemorySlots := shr(5, add(size, 31))
mstore(0, slot)
let uriSlot := keccak256(0, 32)
// Iterate through the words in memory and store the string word by word
// prettier-ignore
for { let i := 0 } lt(i, totalMemorySlots) { i := add(i, 1) } {
mstore(add(add(ptr, 32), mul(32, i)), sload(add(uriSlot, i)))
}
}
// Store the new memory pointer in the free memory pointer slot
mstore(64, add(add(ptr, 32), size))
}
return ptr;
}
function _setDefaultProfile(address wallet, uint256 profileId) private {
if (profileId != 0 && wallet != GeneralHelpers.unsafeOwnerOf(profileId))
revert Errors.NotProfileOwner();
// Store the default profile in the appropriate slot for the given wallet.
assembly {
mstore(0, wallet)
mstore(32, DEFAULT_PROFILE_MAPPING_SLOT)
let slot := keccak256(0, 64)
sstore(slot, profileId)
}
emit Events.DefaultProfileSet(wallet, profileId, block.timestamp);
}
function _setDelegatedExecutorApproval(
address onBehalfOf,
address executor,
bool approved
) private {
// Store the approval in the appropriate slot for the given caller and executor.
assembly {
mstore(0, onBehalfOf)
mstore(32, DELEGATED_EXECUTOR_APPROVAL_MAPPING_SLOT)
mstore(32, keccak256(0, 64))
mstore(0, executor)
let slot := keccak256(0, 64)
sstore(slot, approved)
}
emit Events.DelegatedExecutorApprovalSet(onBehalfOf, executor, approved);
}
function _validateCallerIsOnBehalfOfOrExecutor(address onBehalfOf) private view {
if (onBehalfOf != msg.sender)
GeneralHelpers.validateDelegatedExecutor(onBehalfOf, msg.sender);
}
}

View File

@@ -1,57 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
import {DataTypes} from './DataTypes.sol';
import {Errors} from './Errors.sol';
/**
* @title Helpers
* @author Lens Protocol
*
* @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 tuple First, the pointed publication's publishing profile ID, second, the pointed publication's ID, and third, the
* pointed publication's collect module. If the passed publication is not a mirror, this returns the given publication.
*/
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);
}
}
}

View File

@@ -1,223 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
import {FollowNFTProxy} from '../upgradeability/FollowNFTProxy.sol';
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 Protocol
*
* @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 _profileById A pointer to the storage mapping of profile structs by profile ID.
* @param _profileIdByHandleHash A pointer to the storage mapping of profile IDs by handle hash.
*
* @return uint256[] An array of integers representing the minted follow NFTs token IDs.
*/
function follow(
address follower,
uint256[] calldata profileIds,
bytes[] calldata followModuleDatas,
mapping(uint256 => DataTypes.ProfileStruct) storage _profileById,
mapping(bytes32 => uint256) storage _profileIdByHandleHash
) external returns (uint256[] memory) {
if (profileIds.length != followModuleDatas.length) revert Errors.ArrayMismatch();
uint256[] memory tokenIds = new uint256[](profileIds.length);
for (uint256 i = 0; i < profileIds.length; ) {
string memory handle = _profileById[profileIds[i]].handle;
if (_profileIdByHandleHash[keccak256(bytes(handle))] != profileIds[i])
revert Errors.TokenDoesNotExist();
address followModule = _profileById[profileIds[i]].followModule;
address followNFT = _profileById[profileIds[i]].followNFT;
if (followNFT == address(0)) {
followNFT = _deployFollowNFT(profileIds[i]);
_profileById[profileIds[i]].followNFT = followNFT;
}
tokenIds[i] = IFollowNFT(followNFT).mint(follower);
if (followModule != address(0)) {
IFollowModule(followModule).processFollow(
follower,
profileIds[i],
followModuleDatas[i]
);
}
unchecked {
++i;
}
}
emit Events.Followed(follower, profileIds, followModuleDatas, block.timestamp);
return tokenIds;
}
/**
* @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.
*
* @return uint256 An integer representing the minted token 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 returns (uint256) {
(uint256 rootProfileId, uint256 rootPubId, address rootCollectModule) = Helpers
.getPointedIfMirror(profileId, pubId, _pubByIdByProfile);
uint256 tokenId;
// Avoids stack too deep
{
address collectNFT = _pubByIdByProfile[rootProfileId][rootPubId].collectNFT;
if (collectNFT == address(0)) {
collectNFT = _deployCollectNFT(
rootProfileId,
rootPubId,
_profileById[rootProfileId].handle,
collectNFTImpl
);
_pubByIdByProfile[rootProfileId][rootPubId].collectNFT = collectNFT;
}
tokenId = ICollectNFT(collectNFT).mint(collector);
}
ICollectModule(rootCollectModule).processCollect(
profileId,
collector,
rootProfileId,
rootPubId,
collectModuleData
);
_emitCollectedEvent(
collector,
profileId,
pubId,
rootProfileId,
rootPubId,
collectModuleData
);
return tokenId;
}
/**
* @notice Deploys the given profile's Follow NFT contract.
*
* @param profileId The token ID of the profile which Follow NFT should be deployed.
*
* @return address The address of the deployed Follow NFT contract.
*/
function _deployFollowNFT(uint256 profileId) private returns (address) {
bytes memory functionData = abi.encodeWithSelector(
IFollowNFT.initialize.selector,
profileId
);
address followNFT = address(new FollowNFTProxy(functionData));
emit Events.FollowNFTDeployed(profileId, followNFT, block.timestamp);
return followNFT;
}
/**
* @notice Deploys the given profile's Collect NFT contract.
*
* @param profileId The token ID of the profile which Collect NFT should be deployed.
* @param pubId The publication ID of the publication being collected, which Collect NFT should be deployed.
* @param handle The profile's associated handle.
* @param collectNFTImpl The address of the Collect NFT implementation that should be used for the deployment.
*
* @return address The address of the deployed Collect NFT contract.
*/
function _deployCollectNFT(
uint256 profileId,
uint256 pubId,
string memory handle,
address collectNFTImpl
) private returns (address) {
address collectNFT = Clones.clone(collectNFTImpl);
bytes4 firstBytes = bytes4(bytes(handle));
string memory collectNFTName = string(
abi.encodePacked(handle, Constants.COLLECT_NFT_NAME_INFIX, pubId.toString())
);
string memory collectNFTSymbol = string(
abi.encodePacked(firstBytes, Constants.COLLECT_NFT_SYMBOL_INFIX, pubId.toString())
);
ICollectNFT(collectNFT).initialize(profileId, pubId, collectNFTName, collectNFTSymbol);
emit Events.CollectNFTDeployed(profileId, pubId, collectNFT, block.timestamp);
return collectNFT;
}
/**
* @notice Emits the `Collected` event that signals that a successful collect action has occurred.
*
* @dev This is done through this function to prevent stack too deep compilation error.
*
* @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 data The data passed to the collect module.
*/
function _emitCollectedEvent(
address collector,
uint256 profileId,
uint256 pubId,
uint256 rootProfileId,
uint256 rootPubId,
bytes calldata data
) private {
emit Events.Collected(
collector,
profileId,
pubId,
rootProfileId,
rootPubId,
data,
block.timestamp
);
}
}

View File

@@ -0,0 +1,360 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {GeneralHelpers} from './helpers/GeneralHelpers.sol';
import {MetaTxHelpers} from './helpers/MetaTxHelpers.sol';
import {DataTypes} from './DataTypes.sol';
import {Errors} from './Errors.sol';
import {Events} from './Events.sol';
import {IFollowModule} from '../interfaces/IFollowModule.sol';
import './Constants.sol';
library ProfileLib {
/**
* @notice Creates a profile with the given parameters to the given address. Minting happens
* in the hub.
*
* @param vars The CreateProfileData struct containing the following parameters:
* to: The address receiving the profile.
* imageURI: The URI to set for the profile image.
* followModule: The follow module to use, can be the zero address.
* followModuleInitData: 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).
*/
function createProfile(DataTypes.CreateProfileData calldata vars, uint256 profileId) external {
_validateProfileCreatorWhitelisted();
if (bytes(vars.imageURI).length > MAX_PROFILE_IMAGE_URI_LENGTH)
revert Errors.ProfileImageURILengthInvalid();
_setProfileString(profileId, PROFILE_IMAGE_URI_OFFSET, vars.imageURI);
_setProfileString(profileId, PROFILE_FOLLOW_NFT_URI_OFFSET, vars.followNFTURI);
bytes memory followModuleReturnData;
if (vars.followModule != address(0)) {
// Load the follow module to be used in the next assembly block.
address followModule = vars.followModule;
// Store the follow module for the new profile. We opt not to use the
// _setFollowModule() private function to avoid unnecessary checks.
assembly {
mstore(0, profileId)
mstore(32, PROFILE_BY_ID_MAPPING_SLOT)
let slot := add(keccak256(0, 64), PROFILE_FOLLOW_MODULE_OFFSET)
sstore(slot, followModule)
}
// @note We don't need to check for deprecated modules here because deprecated modules
// are no longer whitelisted.
// Initialize the follow module.
followModuleReturnData = _initFollowModule(
profileId,
vars.to,
vars.followModule,
vars.followModuleInitData
);
}
emit Events.ProfileCreated(
profileId,
msg.sender,
vars.to,
vars.imageURI,
vars.followModule,
followModuleReturnData,
vars.followNFTURI,
block.timestamp
);
}
/**
* @notice Sets the profile image URI for a given profile.
*
* @param profileId The profile ID.
* @param imageURI The image URI to set.
*/
function setProfileImageURI(uint256 profileId, string calldata imageURI) external {
GeneralHelpers.validateCallerIsOwnerOrDispatcherOrExecutor(profileId);
_setProfileImageURI(profileId, imageURI);
}
/**
* @notice Sets the profile image URI via signature for a given profile.
*
* @param vars the SetProfileImageURIWithSigData struct containing the relevant parameters.
*/
function setProfileImageURIWithSig(DataTypes.SetProfileImageURIWithSigData calldata vars)
external
{
uint256 profileId = vars.profileId;
address signer = GeneralHelpers.getOriginatorOrDelegatedExecutorSigner(
GeneralHelpers.unsafeOwnerOf(profileId),
vars.delegatedSigner
);
MetaTxHelpers.baseSetProfileImageURIWithSig(signer, vars);
_setProfileImageURI(vars.profileId, vars.imageURI);
}
/**
* @notice Sets the follow NFT URI for a given profile.
*
* @param profileId The profile ID.
* @param followNFTURI The follow NFT URI to set.
*/
function setFollowNFTURI(uint256 profileId, string calldata followNFTURI) external {
GeneralHelpers.validateCallerIsOwnerOrDispatcherOrExecutor(profileId);
_setFollowNFTURI(profileId, followNFTURI);
}
/**
* @notice Sets the follow NFT URI via signature for a given profile.
*
* @param vars the SetFollowNFTURIWithSigData struct containing the relevant parameters.
*/
function setFollowNFTURIWithSig(DataTypes.SetFollowNFTURIWithSigData calldata vars) external {
uint256 profileId = vars.profileId;
address signer = GeneralHelpers.getOriginatorOrDelegatedExecutorSigner(
GeneralHelpers.unsafeOwnerOf(profileId),
vars.delegatedSigner
);
MetaTxHelpers.baseSetFollowNFTURIWithSig(signer, vars);
_setFollowNFTURI(vars.profileId, vars.followNFTURI);
}
/**
* @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 followModuleInitData The data to pass to the follow module for profile initialization.
*/
function setFollowModule(
uint256 profileId,
address followModule,
bytes calldata followModuleInitData
) external {
GeneralHelpers.validateCallerIsOwnerOrDispatcherOrExecutor(profileId);
_setFollowModule(profileId, msg.sender, followModule, followModuleInitData);
}
/**
* @notice Sets the dispatcher for a given profile via signature.
*
* @param vars the setDispatcherWithSigData struct containing the relevant parameters.
*/
function setDispatcherWithSig(DataTypes.SetDispatcherWithSigData calldata vars) external {
MetaTxHelpers.baseSetDispatcherWithSig(vars);
uint256 profileId = vars.profileId;
address dispatcher = vars.dispatcher;
// Store the dispatcher in the appropriate slot for the given profile ID.
assembly {
mstore(0, profileId)
mstore(32, DISPATCHER_BY_PROFILE_MAPPING_SLOT)
let slot := keccak256(0, 64)
sstore(slot, dispatcher)
}
emit Events.DispatcherSet(profileId, dispatcher, block.timestamp);
}
/**
* @notice sets the follow module via signature for a given profile.
*
* @param vars the SetFollowModuleWithSigData struct containing the relevant parameters.
*/
function setFollowModuleWithSig(DataTypes.SetFollowModuleWithSigData calldata vars) external {
uint256 profileId = vars.profileId;
address signer = GeneralHelpers.getOriginatorOrDelegatedExecutorSigner(
GeneralHelpers.unsafeOwnerOf(profileId),
vars.delegatedSigner
);
MetaTxHelpers.baseSetFollowModuleWithSig(signer, vars);
_setFollowModule(vars.profileId, signer, vars.followModule, vars.followModuleInitData);
}
function setProfileMetadataURI(uint256 profileId, string calldata metadataURI) external {
GeneralHelpers.validateCallerIsOwnerOrDispatcherOrExecutor(profileId);
_setProfileMetadataURI(profileId, metadataURI);
}
function setProfileMetadataURIWithSig(DataTypes.SetProfileMetadataURIWithSigData calldata vars)
external
{
uint256 profileId = vars.profileId;
address signer = GeneralHelpers.getOriginatorOrDelegatedExecutorSigner(
GeneralHelpers.unsafeOwnerOf(profileId),
vars.delegatedSigner
);
MetaTxHelpers.baseSetProfileMetadataURIWithSig(signer, vars);
_setProfileMetadataURI(vars.profileId, vars.metadataURI);
}
function _setProfileString(
uint256 profileId,
uint256 profileOffset,
string calldata value
) private {
assembly {
let length := value.length
let cdOffset := value.offset
mstore(0, profileId)
mstore(32, PROFILE_BY_ID_MAPPING_SLOT)
let slot := add(keccak256(0, 64), profileOffset)
// If the length is greater than 31, storage rules are different.
switch gt(length, 31)
case 1 {
// The length is > 31, so we need to store the actual string in a new slot,
// equivalent to keccak256(startSlot), and store length*2+1 in startSlot.
sstore(slot, add(shl(1, length), 1))
// Calculate the amount of storage slots we need to store the full string.
// This is equivalent to (string.length + 31)/32.
let totalStorageSlots := shr(5, add(length, 31))
// Compute the slot where the actual string will begin, which is the keccak256
// hash of the slot where we stored the modified length.
mstore(0, slot)
slot := keccak256(0, 32)
// Write the actual string to storage starting at the computed slot.
// prettier-ignore
for { let i := 0 } lt(i, totalStorageSlots) { i := add(i, 1) } {
sstore(add(slot, i), calldataload(add(cdOffset, mul(32, i))))
}
}
default {
// The length is <= 31 so store the string and the length*2 in the same slot.
sstore(slot, or(calldataload(cdOffset), shl(1, length)))
}
}
}
function _setProfileMetadataURI(uint256 profileId, string calldata metadataURI) private {
assembly {
let length := metadataURI.length
let cdOffset := metadataURI.offset
mstore(0, profileId)
mstore(32, PROFILE_METADATA_MAPPING_SLOT)
let slot := keccak256(0, 64)
// If the length is greater than 31, storage rules are different.
switch gt(length, 31)
case 1 {
// The length is > 31, so we need to store the actual string in a new slot,
// equivalent to keccak256(startSlot), and store length*2+1 in startSlot.
sstore(slot, add(shl(1, length), 1))
// Calculate the amount of storage slots we need to store the full string.
// This is equivalent to (string.length + 31)/32.
let totalStorageSlots := shr(5, add(length, 31))
// Compute the slot where the actual string will begin, which is the keccak256
// hash of the slot where we stored the modified length.
mstore(0, slot)
slot := keccak256(0, 32)
// Write the actual string to storage starting at the computed slot.
// prettier-ignore
for { let i := 0 } lt(i, totalStorageSlots) { i := add(i, 1) } {
sstore(add(slot, i), calldataload(add(cdOffset, mul(32, i))))
}
}
default {
// The length is <= 31 so store the string and the length*2 in the same slot.
sstore(slot, or(calldataload(cdOffset), shl(1, length)))
}
}
emit Events.ProfileMetadataSet(profileId, metadataURI, block.timestamp);
}
function _setFollowModule(
uint256 profileId,
address executor,
address followModule,
bytes calldata followModuleInitData
) private {
// Store the follow module in the appropriate slot for the given profile ID, but
// only if it is not the same as the previous follow module.
assembly {
mstore(0, profileId)
mstore(32, PROFILE_BY_ID_MAPPING_SLOT)
let slot := add(keccak256(0, 64), PROFILE_FOLLOW_MODULE_OFFSET)
let currentFollowModule := sload(slot)
if iszero(eq(followModule, currentFollowModule)) {
sstore(slot, followModule)
}
}
// Initialize the follow module if it is non-zero.
bytes memory followModuleReturnData;
if (followModule != address(0))
followModuleReturnData = _initFollowModule(
profileId,
executor,
followModule,
followModuleInitData
);
emit Events.FollowModuleSet(
profileId,
followModule,
followModuleReturnData,
block.timestamp
);
}
function _initFollowModule(
uint256 profileId,
address executor,
address followModule,
bytes memory followModuleInitData
) private returns (bytes memory) {
_validateFollowModuleWhitelisted(followModule);
return
IFollowModule(followModule).initializeFollowModule(
profileId,
executor,
followModuleInitData
);
}
function _setProfileImageURI(uint256 profileId, string calldata imageURI) private {
if (bytes(imageURI).length > MAX_PROFILE_IMAGE_URI_LENGTH)
revert Errors.ProfileImageURILengthInvalid();
_setProfileString(profileId, PROFILE_IMAGE_URI_OFFSET, imageURI);
emit Events.ProfileImageURISet(profileId, imageURI, block.timestamp);
}
function _setFollowNFTURI(uint256 profileId, string calldata followNFTURI) private {
_setProfileString(profileId, PROFILE_FOLLOW_NFT_URI_OFFSET, followNFTURI);
emit Events.FollowNFTURISet(profileId, followNFTURI, block.timestamp);
}
function _validateFollowModuleWhitelisted(address followModule) private view {
bool whitelist;
// Load whether the given follow module is whitelisted.
assembly {
mstore(0, followModule)
mstore(32, FOLLOW_MODULE_WHITELIST_MAPPING_SLOT)
let slot := keccak256(0, 64)
whitelist := sload(slot)
}
if (!whitelist) revert Errors.FollowModuleNotWhitelisted();
}
function _validateProfileCreatorWhitelisted() private view {
bool whitelisted;
// Load whether the caller is whitelisted as a profile creator.
assembly {
mstore(0, caller())
mstore(32, PROFILE_CREATOR_WHITELIST_MAPPING_SLOT)
let slot := keccak256(0, 64)
whitelisted := sload(slot)
}
if (!whitelisted) revert Errors.ProfileCreatorNotWhitelisted();
}
}

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import '@openzeppelin/contracts/utils/Base64.sol';
import '@openzeppelin/contracts/utils/Strings.sol';

View File

@@ -0,0 +1,657 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {GeneralHelpers} from './helpers/GeneralHelpers.sol';
import {MetaTxHelpers} from './helpers/MetaTxHelpers.sol';
import {DataTypes} from './DataTypes.sol';
import {Events} from './Events.sol';
import {Errors} from './Errors.sol';
import {ICollectModule} from '../interfaces/ICollectModule.sol';
import {IReferenceModule} from '../interfaces/IReferenceModule.sol';
import {IDeprecatedReferenceModule} from '../interfaces/IDeprecatedReferenceModule.sol';
import './Constants.sol';
library PublishingLib {
/**
* @notice Publishes a post to a given profile.
*
* @param vars The PostData struct.
*
* @return uint256 The created publication's pubId.
*/
function post(DataTypes.PostData calldata vars) external returns (uint256) {
uint256 pubId = _preIncrementPubCount(vars.profileId);
GeneralHelpers.validateCallerIsOwnerOrDispatcherOrExecutor(vars.profileId);
_createPost(
vars.profileId,
msg.sender,
pubId,
vars.contentURI,
vars.collectModule,
vars.collectModuleInitData,
vars.referenceModule,
vars.referenceModuleInitData
);
return pubId;
}
/**
* @notice Publishes a post to a given profile via signature.
*
* @param vars the PostWithSigData struct.
*
* @return uint256 The created publication's pubId.
*/
function postWithSig(DataTypes.PostWithSigData calldata vars) external returns (uint256) {
uint256 profileId = vars.profileId;
address signer = GeneralHelpers.getOriginatorOrDelegatedExecutorSigner(
GeneralHelpers.unsafeOwnerOf(profileId),
vars.delegatedSigner
);
uint256 pubId = _preIncrementPubCount(profileId);
MetaTxHelpers.basePostWithSig(signer, vars);
_createPost(
profileId,
signer,
pubId,
vars.contentURI,
vars.collectModule,
vars.collectModuleInitData,
vars.referenceModule,
vars.referenceModuleInitData
);
return pubId;
}
/**
* @notice Publishes a comment to a given profile via signature.
*
* @param vars the CommentData struct.
*
* @return uint256 The created publication's pubId.
*/
function comment(DataTypes.CommentData calldata vars) external returns (uint256) {
uint256 pubId = _preIncrementPubCount(vars.profileId);
GeneralHelpers.validateCallerIsOwnerOrDispatcherOrExecutor(vars.profileId);
GeneralHelpers.validateNotBlocked(vars.profileId, vars.profileIdPointed);
_createComment(vars, pubId); // caller is executor
return pubId;
}
/**
* @notice Publishes a comment to a given profile via signature.
*
* @param vars the CommentWithSigData struct.
*
* @return uint256 The created publication's pubId.
*/
function commentWithSig(DataTypes.CommentWithSigData calldata vars) external returns (uint256) {
uint256 profileId = vars.profileId;
address signer = GeneralHelpers.getOriginatorOrDelegatedExecutorSigner(
GeneralHelpers.unsafeOwnerOf(profileId),
vars.delegatedSigner
);
uint256 pubId = _preIncrementPubCount(profileId);
GeneralHelpers.validateNotBlocked(vars.profileId, vars.profileIdPointed);
MetaTxHelpers.baseCommentWithSig(signer, vars);
_createCommentWithSigStruct(vars, signer, pubId);
return pubId;
}
/**
* @notice Publishes a mirror to a given profile.
*
* @param vars the MirrorData struct.
*
* @return uint256 The created publication's pubId.
*/
function mirror(DataTypes.MirrorData calldata vars) external returns (uint256) {
uint256 pubId = _preIncrementPubCount(vars.profileId);
GeneralHelpers.validateCallerIsOwnerOrDispatcherOrExecutor(vars.profileId);
GeneralHelpers.validateNotBlocked(vars.profileId, vars.profileIdPointed);
_createMirror(vars, pubId); // caller is executor
return pubId;
}
/**
* @notice Publishes a mirror to a given profile via signature.
*
* @param vars the MirrorWithSigData struct.
*
* @return uint256 The created publication's pubId.
*/
function mirrorWithSig(DataTypes.MirrorWithSigData calldata vars) external returns (uint256) {
uint256 profileId = vars.profileId;
address signer = GeneralHelpers.getOriginatorOrDelegatedExecutorSigner(
GeneralHelpers.unsafeOwnerOf(profileId),
vars.delegatedSigner
);
uint256 pubId = _preIncrementPubCount(profileId);
GeneralHelpers.validateNotBlocked(vars.profileId, vars.profileIdPointed);
MetaTxHelpers.baseMirrorWithSig(signer, vars);
_createMirrorWithSigStruct(vars, signer, pubId);
return pubId;
}
function _preIncrementPubCount(uint256 profileId) private returns (uint256) {
uint256 pubCount;
// Load the previous publication count for the given profile and increment it in storage.
assembly {
mstore(0, profileId)
mstore(32, PROFILE_BY_ID_MAPPING_SLOT)
// pubCount is at offset 0, so we don't need to add any offset.
let slot := keccak256(0, 64)
pubCount := add(sload(slot), 1)
sstore(slot, pubCount)
}
return pubCount;
}
function _setPublicationPointer(
uint256 profileId,
uint256 pubId,
uint256 profileIdPointed,
uint256 pubIdPointed
) private {
// Store the pointed profile ID and pointed pub ID in the appropriate slots for
// a given publication.
assembly {
mstore(0, profileId)
mstore(32, PUB_BY_ID_BY_PROFILE_MAPPING_SLOT)
mstore(32, keccak256(0, 64))
mstore(0, pubId)
// profile ID pointed is at offset 0, so we don't need to add any offset.
let slot := keccak256(0, 64)
sstore(slot, profileIdPointed)
slot := add(slot, PUBLICATION_PUB_ID_POINTED_OFFSET)
sstore(slot, pubIdPointed)
}
}
function _setPublicationContentURI(
uint256 profileId,
uint256 pubId,
string calldata value
) private {
assembly {
let length := value.length
let cdOffset := value.offset
mstore(0, profileId)
mstore(32, PUB_BY_ID_BY_PROFILE_MAPPING_SLOT)
mstore(32, keccak256(0, 64))
mstore(0, pubId)
let slot := add(keccak256(0, 64), PUBLICATION_CONTENT_URI_OFFSET)
// If the length is greater than 31, storage rules are different.
switch gt(length, 31)
case 1 {
// The length is > 31, so we need to store the actual string in a new slot,
// equivalent to keccak256(startSlot), and store length*2+1 in startSlot.
sstore(slot, add(shl(1, length), 1))
// Calculate the amount of storage slots we need to store the full string.
// This is equivalent to (string.length + 31)/32.
let totalStorageSlots := shr(5, add(length, 31))
// Compute the slot where the actual string will begin, which is the keccak256
// hash of the slot where we stored the modified length.
mstore(0, slot)
slot := keccak256(0, 32)
// Write the actual string to storage starting at the computed slot.
// prettier-ignore
for { let i := 0 } lt(i, totalStorageSlots) { i := add(i, 1) } {
sstore(add(slot, i), calldataload(add(cdOffset, mul(32, i))))
}
}
default {
// The length is <= 31 so store the string and the length*2 in the same slot.
sstore(slot, or(calldataload(cdOffset), shl(1, length)))
}
}
}
/**
* @notice Creates a post publication mapped to the given profile.
*
* @param profileId The profile ID to associate this publication to.
* @param executor The executor, which is either the owner or an approved delegated executor.
* @param pubId The publication ID to associate with this publication.
* @param contentURI The URI to set for this publication.
* @param collectModule The collect module to set for this publication.
* @param collectModuleInitData The data to pass to the collect module for publication initialization.
* @param referenceModule The reference module to set for this publication, if any.
* @param referenceModuleInitData The data to pass to the reference module for publication initialization.
*/
function _createPost(
uint256 profileId,
address executor,
uint256 pubId,
string calldata contentURI,
address collectModule,
bytes calldata collectModuleInitData,
address referenceModule,
bytes calldata referenceModuleInitData
) private {
_setPublicationContentURI(profileId, pubId, contentURI);
bytes memory collectModuleReturnData = _initPubCollectModule(
profileId,
executor,
pubId,
collectModule,
collectModuleInitData
);
bytes memory referenceModuleReturnData = _initPubReferenceModule(
profileId,
executor,
pubId,
referenceModule,
referenceModuleInitData
);
emit Events.PostCreated(
profileId,
pubId,
contentURI,
collectModule,
collectModuleReturnData,
referenceModule,
referenceModuleReturnData,
block.timestamp
);
}
/**
* @notice Creates a comment publication mapped to the given profile.
*
* @param vars The CommentData struct to use to create the comment.
* @param pubId The publication ID to associate with this publication.
*/
function _createComment(DataTypes.CommentData calldata vars, uint256 pubId) private {
uint256 pubCountPointed = _getPubCount(vars.profileIdPointed);
if (pubCountPointed < vars.pubIdPointed || vars.pubIdPointed == 0)
revert Errors.PublicationDoesNotExist();
if (vars.profileId == vars.profileIdPointed && vars.pubIdPointed == pubId)
revert Errors.CannotCommentOnSelf();
_setPublicationPointer(vars.profileId, pubId, vars.profileIdPointed, vars.pubIdPointed);
_setPublicationContentURI(vars.profileId, pubId, vars.contentURI);
bytes memory collectModuleReturnData = _initPubCollectModule(
vars.profileId,
msg.sender,
pubId,
vars.collectModule,
vars.collectModuleInitData
);
bytes memory referenceModuleReturnData = _initPubReferenceModule(
vars.profileId,
msg.sender,
pubId,
vars.referenceModule,
vars.referenceModuleInitData
);
_processCommentIfNeeded(
vars.profileId,
msg.sender,
vars.profileIdPointed,
vars.pubIdPointed,
vars.referenceModuleData
);
emit Events.CommentCreated(
vars.profileId,
pubId,
vars.contentURI,
vars.profileIdPointed,
vars.pubIdPointed,
vars.referenceModuleData,
vars.collectModule,
collectModuleReturnData,
vars.referenceModule,
referenceModuleReturnData,
block.timestamp
);
}
/**
* @notice Creates a comment publication mapped to the given profile with a sig struct.
*
* @param vars The CommentWithSigData struct to use to create the comment.
* @param executor The publisher or an approved delegated executor.
* @param pubId The publication ID to associate with this publication.
*/
function _createCommentWithSigStruct(
DataTypes.CommentWithSigData calldata vars,
address executor,
uint256 pubId
) private {
// Prevents stack too deep.
{
uint256 pubCountPointed = _getPubCount(vars.profileIdPointed);
if (pubCountPointed < vars.pubIdPointed || vars.pubIdPointed == 0)
revert Errors.PublicationDoesNotExist();
}
if (vars.profileId == vars.profileIdPointed && vars.pubIdPointed == pubId)
revert Errors.CannotCommentOnSelf();
_setPublicationPointer(vars.profileId, pubId, vars.profileIdPointed, vars.pubIdPointed);
_setPublicationContentURI(vars.profileId, pubId, vars.contentURI);
bytes memory collectModuleReturnData = _initPubCollectModule(
vars.profileId,
executor,
pubId,
vars.collectModule,
vars.collectModuleInitData
);
bytes memory referenceModuleReturnData = _initPubReferenceModule(
vars.profileId,
executor,
pubId,
vars.referenceModule,
vars.referenceModuleInitData
);
_processCommentIfNeeded(
vars.profileId,
executor,
vars.profileIdPointed,
vars.pubIdPointed,
vars.referenceModuleData
);
emit Events.CommentCreated(
vars.profileId,
pubId,
vars.contentURI,
vars.profileIdPointed,
vars.pubIdPointed,
vars.referenceModuleData,
vars.collectModule,
collectModuleReturnData,
vars.referenceModule,
referenceModuleReturnData,
block.timestamp
);
}
/**
* @notice Creates a mirror publication mapped to the given profile.
*
* @param vars The MirrorData struct to use to create the mirror.
* @param pubId The publication ID to associate with this publication.
*/
function _createMirror(DataTypes.MirrorData calldata vars, uint256 pubId) private {
(uint256 rootProfileIdPointed, uint256 rootPubIdPointed) = GeneralHelpers
.getPointedIfMirror(vars.profileIdPointed, vars.pubIdPointed);
_setPublicationPointer(vars.profileId, pubId, rootProfileIdPointed, rootPubIdPointed);
bytes memory referenceModuleReturnData = _initPubReferenceModule(
vars.profileId,
msg.sender,
pubId,
vars.referenceModule,
vars.referenceModuleInitData
);
_processMirrorIfNeeded(
vars.profileId,
msg.sender,
rootProfileIdPointed,
rootPubIdPointed,
vars.referenceModuleData
);
emit Events.MirrorCreated(
vars.profileId,
pubId,
rootProfileIdPointed,
rootPubIdPointed,
vars.referenceModuleData,
vars.referenceModule,
referenceModuleReturnData,
block.timestamp
);
}
/**
* @notice Creates a mirror publication mapped to the given profile using a sig struct.
*
* @param vars The MirrorWithSigData struct to use to create the mirror.
* @param executor The publisher or an approved delegated executor.
* @param pubId The publication ID to associate with this publication.
*/
function _createMirrorWithSigStruct(
DataTypes.MirrorWithSigData calldata vars,
address executor,
uint256 pubId
) private {
(uint256 rootProfileIdPointed, uint256 rootPubIdPointed) = GeneralHelpers
.getPointedIfMirror(vars.profileIdPointed, vars.pubIdPointed);
_setPublicationPointer(vars.profileId, pubId, rootProfileIdPointed, rootPubIdPointed);
bytes memory referenceModuleReturnData = _initPubReferenceModule(
vars.profileId,
executor,
pubId,
vars.referenceModule,
vars.referenceModuleInitData
);
_processMirrorIfNeeded(
vars.profileId,
executor,
rootProfileIdPointed,
rootPubIdPointed,
vars.referenceModuleData
);
emit Events.MirrorCreated(
vars.profileId,
pubId,
rootProfileIdPointed,
rootPubIdPointed,
vars.referenceModuleData,
vars.referenceModule,
referenceModuleReturnData,
block.timestamp
);
}
function _validateCollectModuleWhitelisted(address collectModule) private view {
bool whitelisted;
// Load whether the given collect module is whitelisted.
assembly {
mstore(0, collectModule)
mstore(32, COLLECT_MODULE_WHITELIST_MAPPING_SLOT)
let slot := keccak256(0, 64)
whitelisted := sload(slot)
}
if (!whitelisted) revert Errors.CollectModuleNotWhitelisted();
}
function _validateReferenceModuleWhitelisted(address referenceModule) private view {
bool whitelisted;
// Load whether the given reference module is whitelisted.
assembly {
mstore(0, referenceModule)
mstore(32, REFERENCE_MODULE_WHITELIST_MAPPING_SLOT)
let slot := keccak256(0, 64)
whitelisted := sload(slot)
}
if (!whitelisted) revert Errors.ReferenceModuleNotWhitelisted();
}
function _processCommentIfNeeded(
uint256 profileId,
address executor,
uint256 profileIdPointed,
uint256 pubIdPointed,
bytes calldata referenceModuleData
) private {
address refModule = _getReferenceModule(profileIdPointed, pubIdPointed);
if (refModule != address(0)) {
try
IReferenceModule(refModule).processComment(
profileId,
executor,
profileIdPointed,
pubIdPointed,
referenceModuleData
)
{} catch (bytes memory err) {
assembly {
/// Equivalent to reverting with the returned error selector if
/// the length is not zero.
let length := mload(err)
if iszero(iszero(length)) {
revert(add(err, 32), length)
}
}
if (executor != GeneralHelpers.unsafeOwnerOf(profileId))
revert Errors.ExecutorInvalid();
IDeprecatedReferenceModule(refModule).processComment(
profileId,
profileIdPointed,
pubIdPointed,
referenceModuleData
);
}
}
}
function _processMirrorIfNeeded(
uint256 profileId,
address executor,
uint256 profileIdPointed,
uint256 pubIdPointed,
bytes calldata referenceModuleData
) private {
address refModule = _getReferenceModule(profileIdPointed, pubIdPointed);
if (refModule != address(0)) {
try
IReferenceModule(refModule).processMirror(
profileId,
executor,
profileIdPointed,
pubIdPointed,
referenceModuleData
)
{} catch (bytes memory err) {
assembly {
/// Equivalent to reverting with the returned error selector if
/// the length is not zero.
let length := mload(err)
if iszero(iszero(length)) {
revert(add(err, 32), length)
}
}
if (executor != GeneralHelpers.unsafeOwnerOf(profileId))
revert Errors.ExecutorInvalid();
IDeprecatedReferenceModule(refModule).processMirror(
profileId,
profileIdPointed,
pubIdPointed,
referenceModuleData
);
}
}
}
function _initPubCollectModule(
uint256 profileId,
address executor,
uint256 pubId,
address collectModule,
bytes memory collectModuleInitData
) private returns (bytes memory) {
_validateCollectModuleWhitelisted(collectModule);
// Store the collect module in the appropriate slot for the given publication.
assembly {
mstore(0, profileId)
mstore(32, PUB_BY_ID_BY_PROFILE_MAPPING_SLOT)
mstore(32, keccak256(0, 64))
mstore(0, pubId)
let slot := add(keccak256(0, 64), PUBLICATION_COLLECT_MODULE_OFFSET)
sstore(slot, collectModule)
}
return
ICollectModule(collectModule).initializePublicationCollectModule(
profileId,
executor,
pubId,
collectModuleInitData
);
}
function _initPubReferenceModule(
uint256 profileId,
address executor,
uint256 pubId,
address referenceModule,
bytes memory referenceModuleInitData
) private returns (bytes memory) {
if (referenceModule == address(0)) return new bytes(0);
_validateReferenceModuleWhitelisted(referenceModule);
// Store the reference module in the appropriate slot for the given publication.
assembly {
mstore(0, profileId)
mstore(32, PUB_BY_ID_BY_PROFILE_MAPPING_SLOT)
mstore(32, keccak256(0, 64))
mstore(0, pubId)
let slot := add(keccak256(0, 64), PUBLICATION_REFERENCE_MODULE_OFFSET)
sstore(slot, referenceModule)
}
return
IReferenceModule(referenceModule).initializeReferenceModule(
profileId,
executor,
pubId,
referenceModuleInitData
);
}
function _getPubCount(uint256 profileId) private view returns (uint256) {
uint256 pubCount;
// Load the publication count for the given profile.
assembly {
mstore(0, profileId)
mstore(32, PROFILE_BY_ID_MAPPING_SLOT)
// pubCount is at offset 0, so we don't need to add any offset.
let slot := keccak256(0, 64)
pubCount := sload(slot)
}
return pubCount;
}
function _getReferenceModule(uint256 profileId, uint256 pubId) private view returns (address) {
address referenceModule;
// Load the reference module for the given publication.
assembly {
mstore(0, profileId)
mstore(32, PUB_BY_ID_BY_PROFILE_MAPPING_SLOT)
mstore(32, keccak256(0, 64))
mstore(0, pubId)
let slot := add(keccak256(0, 64), PUBLICATION_REFERENCE_MODULE_OFFSET)
referenceModule := sload(slot)
}
return referenceModule;
}
}

View File

@@ -1,411 +0,0 @@
// SPDX-License-Identifier: MIT
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 Protocol
*
* @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.
* followModuleInitData: 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);
if (bytes(vars.imageURI).length > Constants.MAX_PROFILE_IMAGE_URI_LENGTH)
revert Errors.ProfileImageURILengthInvalid();
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;
bytes memory followModuleReturnData;
if (vars.followModule != address(0)) {
_profileById[profileId].followModule = vars.followModule;
followModuleReturnData = _initFollowModule(
profileId,
vars.followModule,
vars.followModuleInitData,
_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 followModuleInitData 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 followModuleInitData,
DataTypes.ProfileStruct storage _profile,
mapping(address => bool) storage _followModuleWhitelisted
) external {
if (followModule != _profile.followModule) {
_profile.followModule = followModule;
}
bytes memory followModuleReturnData;
if (followModule != address(0))
followModuleReturnData = _initFollowModule(
profileId,
followModule,
followModuleInitData,
_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 collectModuleInitData The data to pass to the collect module for publication initialization.
* @param referenceModule The reference module to set for this publication, if any.
* @param referenceModuleInitData 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 collectModuleInitData,
address referenceModule,
bytes memory referenceModuleInitData,
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,
collectModuleInitData,
_pubByIdByProfile,
_collectModuleWhitelisted
);
// Reference module initialization
bytes memory referenceModuleReturnData = _initPubReferenceModule(
profileId,
pubId,
referenceModule,
referenceModuleInitData,
_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();
// Ensure the pointed publication is not the comment being created
if (vars.profileId == vars.profileIdPointed && vars.pubIdPointed == pubId)
revert Errors.CannotCommentOnSelf();
_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.collectModuleInitData,
_pubByIdByProfile,
_collectModuleWhitelisted
);
// Reference module initialization
bytes memory referenceModuleReturnData = _initPubReferenceModule(
vars.profileId,
pubId,
vars.referenceModule,
vars.referenceModuleInitData,
_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,
vars.referenceModuleData
);
}
// Prevents a stack too deep error
_emitCommentCreated(vars, pubId, collectModuleReturnData, referenceModuleReturnData);
}
/**
* @notice Creates a mirror publication mapped to the given profile.
*
* @param vars The MirrorData struct to use to create the mirror.
* @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(
DataTypes.MirrorData memory vars,
uint256 pubId,
mapping(uint256 => mapping(uint256 => DataTypes.PublicationStruct))
storage _pubByIdByProfile,
mapping(address => bool) storage _referenceModuleWhitelisted
) external {
(uint256 rootProfileIdPointed, uint256 rootPubIdPointed, ) = Helpers.getPointedIfMirror(
vars.profileIdPointed,
vars.pubIdPointed,
_pubByIdByProfile
);
_pubByIdByProfile[vars.profileId][pubId].profileIdPointed = rootProfileIdPointed;
_pubByIdByProfile[vars.profileId][pubId].pubIdPointed = rootPubIdPointed;
// Reference module initialization
bytes memory referenceModuleReturnData = _initPubReferenceModule(
vars.profileId,
pubId,
vars.referenceModule,
vars.referenceModuleInitData,
_pubByIdByProfile,
_referenceModuleWhitelisted
);
// Reference module validation
address refModule = _pubByIdByProfile[rootProfileIdPointed][rootPubIdPointed]
.referenceModule;
if (refModule != address(0)) {
IReferenceModule(refModule).processMirror(
vars.profileId,
rootProfileIdPointed,
rootPubIdPointed,
vars.referenceModuleData
);
}
emit Events.MirrorCreated(
vars.profileId,
pubId,
rootProfileIdPointed,
rootPubIdPointed,
vars.referenceModuleData,
vars.referenceModule,
referenceModuleReturnData,
block.timestamp
);
}
function _initPubCollectModule(
uint256 profileId,
uint256 pubId,
address collectModule,
bytes memory collectModuleInitData,
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,
collectModuleInitData
);
}
function _initPubReferenceModule(
uint256 profileId,
uint256 pubId,
address referenceModule,
bytes memory referenceModuleInitData,
mapping(uint256 => mapping(uint256 => DataTypes.PublicationStruct))
storage _pubByIdByProfile,
mapping(address => bool) storage _referenceModuleWhitelisted
) private returns (bytes memory) {
if (referenceModule == address(0)) return new bytes(0);
if (!_referenceModuleWhitelisted[referenceModule])
revert Errors.ReferenceModuleNotWhitelisted();
_pubByIdByProfile[profileId][pubId].referenceModule = referenceModule;
return
IReferenceModule(referenceModule).initializeReferenceModule(
profileId,
pubId,
referenceModuleInitData
);
}
function _initFollowModule(
uint256 profileId,
address followModule,
bytes memory followModuleInitData,
mapping(address => bool) storage _followModuleWhitelisted
) private returns (bytes memory) {
if (!_followModuleWhitelisted[followModule]) revert Errors.FollowModuleNotWhitelisted();
return IFollowModule(followModule).initializeFollowModule(profileId, followModuleInitData);
}
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.referenceModuleData,
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();
uint256 byteHandleLength = byteHandle.length;
for (uint256 i = 0; i < byteHandleLength; ) {
if (
(byteHandle[i] < '0' ||
byteHandle[i] > 'z' ||
(byteHandle[i] > '9' && byteHandle[i] < 'a')) &&
byteHandle[i] != '.' &&
byteHandle[i] != '-' &&
byteHandle[i] != '_'
) revert Errors.HandleContainsInvalidCharacters();
unchecked {
++i;
}
}
}
}

View File

@@ -0,0 +1,253 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {DataTypes} from '../DataTypes.sol';
import {Errors} from '../Errors.sol';
import '../Constants.sol';
/**
* @title Helpers
* @author Lens Protocol
*
* @notice This is a library that contains helper internal functions used by both the Hub and the GeneralLib.
*/
library GeneralHelpers {
/**
* @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.
*
* @return tuple First, the pointed publication's publishing profile ID, and second, the pointed publication's ID.
* If the passed publication is not a mirror, this returns the given publication.
*/
function getPointedIfMirror(uint256 profileId, uint256 pubId)
internal
view
returns (uint256, uint256)
{
uint256 slot;
address collectModule;
// Load the collect module for the given profile (zero if it is a mirror) and cache the
// publication storage slot.
assembly {
mstore(0, profileId)
mstore(32, PUB_BY_ID_BY_PROFILE_MAPPING_SLOT)
mstore(32, keccak256(0, 64))
mstore(0, pubId)
slot := keccak256(0, 64)
let collectModuleSlot := add(slot, PUBLICATION_COLLECT_MODULE_OFFSET)
collectModule := sload(collectModuleSlot)
}
if (collectModule != address(0)) {
return (profileId, pubId);
} else {
uint256 profileIdPointed;
// Load the pointed profile ID, first in the cached slot.
assembly {
// profile ID pointed is at offset 0, so we don't need to add any offset.
profileIdPointed := sload(slot)
}
// We validate existence here as an optimization, so validating in calling
// contracts is unnecessary.
if (profileIdPointed == 0) revert Errors.PublicationDoesNotExist();
uint256 pubIdPointed;
// Load the pointed publication ID for the given publication.
assembly {
let pointedPubIdSlot := add(slot, PUBLICATION_PUB_ID_POINTED_OFFSET)
pubIdPointed := sload(pointedPubIdSlot)
}
return (profileIdPointed, pubIdPointed);
}
}
/**
* @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.
*
* @return tuple First, the pointed publication's publishing profile ID, second, the pointed publication's ID, and third, the
* pointed publication's collect module. If the passed publication is not a mirror, this returns the given publication.
*/
function getPointedIfMirrorWithCollectModule(uint256 profileId, uint256 pubId)
internal
view
returns (
uint256,
uint256,
address
)
{
uint256 slot;
address collectModule;
// Load the collect module for the given profile (zero if it is a mirror) and cache the
// publication storage slot.
assembly {
mstore(0, profileId)
mstore(32, PUB_BY_ID_BY_PROFILE_MAPPING_SLOT)
mstore(32, keccak256(0, 64))
mstore(0, pubId)
slot := keccak256(0, 64)
let collectModuleSlot := add(slot, PUBLICATION_COLLECT_MODULE_OFFSET)
collectModule := sload(collectModuleSlot)
}
if (collectModule != address(0)) {
return (profileId, pubId, collectModule);
} else {
uint256 profileIdPointed;
// Load the pointed profile ID, first in the cached slot.
assembly {
// profile ID pointed is at offset 0, so we don't need to add any offset.
profileIdPointed := sload(slot)
}
// We validate existence here as an optimization, so validating in calling
// contracts is unnecessary.
if (profileIdPointed == 0) revert Errors.PublicationDoesNotExist();
uint256 pubIdPointed;
address collectModulePointed;
// Load the pointed publication ID and the pointed collect module for the given
// publication.
assembly {
let pointedPubIdSlot := add(slot, PUBLICATION_PUB_ID_POINTED_OFFSET)
pubIdPointed := sload(pointedPubIdSlot)
mstore(0, profileIdPointed)
mstore(32, PUB_BY_ID_BY_PROFILE_MAPPING_SLOT)
mstore(32, keccak256(0, 64))
mstore(0, pubIdPointed)
slot := add(keccak256(0, 64), PUBLICATION_COLLECT_MODULE_OFFSET)
collectModulePointed := sload(slot)
}
return (profileIdPointed, pubIdPointed, collectModulePointed);
}
}
/**
* @dev This fetches the owner address for a given token ID. Note that this does not check and
* revert upon loading a zero address.
*
* However, this function is only used if the result is compared to the caller or a recovered signer,
* which is already checked for the zero address.
*/
function unsafeOwnerOf(uint256 tokenId) internal view returns (address) {
address owner;
assembly {
mstore(0, tokenId)
mstore(32, TOKEN_DATA_MAPPING_SLOT)
let slot := keccak256(0, 64)
// This bit shift is necessary to remove the packing from the variable.
owner := shr(96, shl(96, sload(slot)))
}
return owner;
}
function ownerOf(uint256 tokenId) internal view returns (address) {
address owner = unsafeOwnerOf(tokenId);
if (owner == address(0)) {
revert Errors.TokenDoesNotExist();
}
return owner;
}
function validateCallerIsOwnerOrDispatcherOrExecutor(uint256 profileId) internal view {
// It's safe to use the `unsafeOwnerOf()` function here because the sender cannot be
// the zero address, the dispatcher is cleared on burn and the zero address cannot approve
// a delegated executor.
address owner = unsafeOwnerOf(profileId);
if (msg.sender != owner) {
address dispatcher;
// Load the dispatcher for the given profile.
assembly {
mstore(0, profileId)
mstore(32, DISPATCHER_BY_PROFILE_MAPPING_SLOT)
let slot := keccak256(0, 64)
dispatcher := sload(slot)
}
if (msg.sender == dispatcher) return;
validateDelegatedExecutor(owner, msg.sender);
}
}
function validateCallerIsOwnerOrDelegatedExecutor(uint256 profileId) internal view {
address owner = ownerOf(profileId);
if (msg.sender != owner) {
validateDelegatedExecutor(owner, msg.sender);
}
}
function validateAddressIsOwnerOrDelegatedExecutor(
address transactionExecutor,
address profileOwner
) internal view {
if (transactionExecutor != profileOwner) {
validateDelegatedExecutor(profileOwner, transactionExecutor);
}
}
function validateDelegatedExecutor(address onBehalfOf, address executor) internal view {
if (!isExecutorApproved(onBehalfOf, executor)) {
revert Errors.ExecutorInvalid();
}
}
function isExecutorApproved(address onBehalfOf, address executor) internal view returns (bool) {
bool isExecutorApproved;
assembly {
mstore(0, onBehalfOf)
mstore(32, DELEGATED_EXECUTOR_APPROVAL_MAPPING_SLOT)
mstore(32, keccak256(0, 64))
mstore(0, executor)
let slot := keccak256(0, 64)
isExecutorApproved := sload(slot)
}
return isExecutorApproved;
}
/**
* @dev Returns either the profile owner or the delegated signer if valid.
*/
function getOriginatorOrDelegatedExecutorSigner(address originator, address delegatedSigner)
internal
view
returns (address)
{
if (delegatedSigner != address(0)) {
validateDelegatedExecutor(originator, delegatedSigner);
return delegatedSigner;
}
return originator;
}
function validateNotBlocked(uint256 profile, uint256 byProfile) internal view {
bool isBlocked;
assembly {
mstore(0, byProfile)
mstore(32, BLOCK_STATUS_MAPPING_SLOT)
let blockStatusByProfileSlot := keccak256(0, 64)
mstore(0, profile)
mstore(32, blockStatusByProfileSlot)
isBlocked := sload(keccak256(0, 64))
}
if (isBlocked) {
revert Errors.Blocked();
}
}
}

View File

@@ -0,0 +1,432 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {FollowNFTProxy} from '../../upgradeability/FollowNFTProxy.sol';
import {GeneralHelpers} from './GeneralHelpers.sol';
import {DataTypes} from '../DataTypes.sol';
import {Errors} from '../Errors.sol';
import {Events} from '../Events.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 {IReferenceModule} from '../../interfaces/IReferenceModule.sol';
import {IDeprecatedFollowModule} from '../../interfaces/IDeprecatedFollowModule.sol';
import {IDeprecatedCollectModule} from '../../interfaces/IDeprecatedCollectModule.sol';
import {IDeprecatedReferenceModule} from '../../interfaces/IDeprecatedReferenceModule.sol';
import {Clones} from '@openzeppelin/contracts/proxy/Clones.sol';
import {Strings} from '@openzeppelin/contracts/utils/Strings.sol';
import '../Constants.sol';
/**
* @title InteractionHelpers
* @author Lens Protocol
*
* @notice This is the library used by the GeneralLib that contains the logic for follows & collects.
*
* @dev The functions are internal, so they are inlined into the GeneralLib.
*/
library InteractionHelpers {
using Strings for uint256;
function follow(
uint256 followerProfileId,
address executor,
uint256[] calldata idsOfProfilesToFollow,
uint256[] calldata followTokenIds,
bytes[] calldata followModuleDatas
) internal returns (uint256[] memory) {
if (
idsOfProfilesToFollow.length != followTokenIds.length ||
idsOfProfilesToFollow.length != followModuleDatas.length
) {
revert Errors.ArrayMismatch();
}
uint256[] memory followTokenIdsAssigned = new uint256[](idsOfProfilesToFollow.length);
uint256 i;
while (i < idsOfProfilesToFollow.length) {
_validateProfileExists({profileId: idsOfProfilesToFollow[i]});
GeneralHelpers.validateNotBlocked({
profile: followerProfileId,
byProfile: idsOfProfilesToFollow[i]
});
if (followerProfileId == idsOfProfilesToFollow[i]) {
revert Errors.SelfFollow();
}
followTokenIdsAssigned[i] = _follow({
followerProfileId: followerProfileId,
executor: executor,
idOfProfileToFollow: idsOfProfilesToFollow[i],
followTokenId: followTokenIds[i],
followModuleData: followModuleDatas[i]
});
unchecked {
++i;
}
}
return followTokenIdsAssigned;
}
function unfollow(
uint256 unfollowerProfileId,
address executor,
uint256[] calldata idsOfProfilesToUnfollow
) internal {
uint256 i;
while (i < idsOfProfilesToUnfollow.length) {
uint256 idOfProfileToUnfollow = idsOfProfilesToUnfollow[i];
_validateProfileExists(idOfProfileToUnfollow);
address followNFT;
// Load the Follow NFT for the profile being unfollowed.
assembly {
mstore(0, idOfProfileToUnfollow)
mstore(32, PROFILE_BY_ID_MAPPING_SLOT)
let followNFTSlot := add(keccak256(0, 64), PROFILE_FOLLOW_NFT_OFFSET)
followNFT := sload(followNFTSlot)
}
if (followNFT == address(0)) {
revert Errors.NotFollowing();
}
IFollowNFT(followNFT).unfollow({
unfollowerProfileId: unfollowerProfileId,
executor: executor
});
emit Events.Unfollowed(unfollowerProfileId, idOfProfileToUnfollow, block.timestamp);
unchecked {
++i;
}
}
}
function setBlockStatus(
uint256 byProfileId,
uint256[] calldata idsOfProfilesToSetBlockStatus,
bool[] calldata blockStatus
) internal {
if (idsOfProfilesToSetBlockStatus.length != blockStatus.length) {
revert Errors.ArrayMismatch();
}
uint256 blockStatusByProfileSlot;
// Calculates the slot of the block status internal mapping once accessed by `byProfileId`.
// i.e. the slot of `_blockedStatus[byProfileId]`
assembly {
mstore(0, byProfileId)
mstore(32, BLOCK_STATUS_MAPPING_SLOT)
blockStatusByProfileSlot := keccak256(0, 64)
}
address followNFT;
// Loads the Follow NFT address from storage.
// i.e. `followNFT = _profileById[byProfileId].followNFT;`
assembly {
mstore(0, byProfileId)
mstore(32, PROFILE_BY_ID_MAPPING_SLOT)
followNFT := sload(add(keccak256(0, 64), PROFILE_FOLLOW_NFT_OFFSET))
}
uint256 i;
uint256 idOfProfileToSetBlockStatus;
bool setToBlocked;
while (i < idsOfProfilesToSetBlockStatus.length) {
idOfProfileToSetBlockStatus = idsOfProfilesToSetBlockStatus[i];
_validateProfileExists(idOfProfileToSetBlockStatus);
if (byProfileId == idOfProfileToSetBlockStatus) {
revert Errors.SelfBlock();
}
setToBlocked = blockStatus[i];
if (followNFT != address(0) && setToBlocked) {
IFollowNFT(followNFT).processBlock(idOfProfileToSetBlockStatus);
}
// Stores the block status.
// i.e. `_blockedStatus[byProfileId][idOfProfileToSetBlockStatus] = setToBlocked;`
assembly {
mstore(0, idOfProfileToSetBlockStatus)
mstore(32, blockStatusByProfileSlot)
sstore(keccak256(0, 64), setToBlocked)
}
if (setToBlocked) {
emit Events.Blocked(byProfileId, idOfProfileToSetBlockStatus, block.timestamp);
} else {
emit Events.Unblocked(byProfileId, idOfProfileToSetBlockStatus, block.timestamp);
}
unchecked {
++i;
}
}
}
function collect(
uint256 collectorProfileId,
address collectorProfileOwner,
address transactionExecutor, // TODO: (ex-delegatedExecutor) - revisit the naming later
uint256 publisherProfileId,
uint256 pubId,
bytes calldata collectModuleData,
address collectNFTImpl
) internal returns (uint256) {
uint256 publisherProfileIdCached = publisherProfileId;
uint256 pubIdCached = pubId;
uint256 collectorProfileIdCached = collectorProfileId;
address collectorProfileOwnerCached = collectorProfileOwner;
address transactionExecutorCached = transactionExecutor;
GeneralHelpers.validateAddressIsOwnerOrDelegatedExecutor({
transactionExecutor: transactionExecutor,
profileOwner: collectorProfileOwner
});
GeneralHelpers.validateNotBlocked(collectorProfileId, publisherProfileId);
(uint256 rootProfileId, uint256 rootPubId, address rootCollectModule) = GeneralHelpers
.getPointedIfMirrorWithCollectModule(publisherProfileIdCached, pubIdCached);
// Prevents stack too deep.
address collectNFT;
{
uint256 collectNFTSlot;
// Load the collect NFT and for the given publication being collected, and cache the
// collect NFT slot.
assembly {
mstore(0, rootProfileId)
mstore(32, PUB_BY_ID_BY_PROFILE_MAPPING_SLOT)
mstore(32, keccak256(0, 64))
mstore(0, rootPubId)
collectNFTSlot := add(keccak256(0, 64), PUBLICATION_COLLECT_NFT_OFFSET)
collectNFT := sload(collectNFTSlot)
}
if (collectNFT == address(0)) {
collectNFT = _deployCollectNFT(rootProfileId, rootPubId, collectNFTImpl);
// Store the collect NFT in the cached slot.
assembly {
sstore(collectNFTSlot, collectNFT)
}
}
}
uint256 tokenId = ICollectNFT(collectNFT).mint(collectorProfileOwnerCached);
_processCollect(
ProcessCollectVars({
collectModule: rootCollectModule,
publisherProfileId: publisherProfileIdCached,
collectorProfileId: collectorProfileIdCached,
collectorProfileOwner: collectorProfileOwnerCached,
transactionExecutor: transactionExecutorCached,
rootProfileId: rootProfileId,
rootPubId: rootPubId,
pubId: pubIdCached
}),
collectModuleData
);
return tokenId;
}
// TODO: Think about how to make this better... (it's needed for stack too deep)
struct ProcessCollectVars {
address collectModule;
uint256 publisherProfileId;
uint256 collectorProfileId;
address collectorProfileOwner;
address transactionExecutor;
uint256 rootProfileId;
uint256 rootPubId;
uint256 pubId;
}
function _processCollect(ProcessCollectVars memory vars, bytes calldata collectModuleData)
private
{
try
ICollectModule(vars.collectModule).processCollect(
vars.publisherProfileId,
vars.collectorProfileId,
vars.collectorProfileOwner,
vars.transactionExecutor,
vars.rootProfileId,
vars.rootPubId,
collectModuleData
)
{} catch (bytes memory err) {
assembly {
/// Equivalent to reverting with the returned error selector if
/// the length is not zero.
let length := mload(err)
if iszero(iszero(length)) {
revert(add(err, 32), length)
}
}
if (vars.collectorProfileOwner != vars.transactionExecutor)
revert Errors.ExecutorInvalid();
IDeprecatedCollectModule(vars.collectModule).processCollect(
vars.publisherProfileId,
vars.collectorProfileOwner,
vars.rootProfileId,
vars.rootPubId,
collectModuleData
);
}
_emitCollectedEvent(
vars.collectorProfileId,
vars.publisherProfileId,
vars.pubId,
vars.rootProfileId,
vars.rootPubId,
collectModuleData
);
}
/**
* @notice Deploys the given profile's Collect NFT contract.
*
* @param profileId The token ID of the profile which Collect NFT should be deployed.
* @param pubId The publication ID of the publication being collected, which Collect NFT should be deployed.
* @param collectNFTImpl The address of the Collect NFT implementation that should be used for the deployment.
*
* @return address The address of the deployed Collect NFT contract.
*/
function _deployCollectNFT(
uint256 profileId,
uint256 pubId,
address collectNFTImpl
) private returns (address) {
address collectNFT = Clones.clone(collectNFTImpl);
string memory collectNFTName = string(
abi.encodePacked(profileId.toString(), COLLECT_NFT_NAME_INFIX, pubId.toString())
);
string memory collectNFTSymbol = string(
abi.encodePacked(profileId.toString(), COLLECT_NFT_SYMBOL_INFIX, pubId.toString())
);
ICollectNFT(collectNFT).initialize(profileId, pubId, collectNFTName, collectNFTSymbol);
emit Events.CollectNFTDeployed(profileId, pubId, collectNFT, block.timestamp);
return collectNFT;
}
/**
* @notice Emits the `Collected` event that signals that a successful collect action has occurred.
*
* @dev This is done through this function to prevent stack too deep compilation error.
*
* @param collectorProfileId The owner address of the profile collecting the publication.
* @param publisherProfileId 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 data The data passed to the collect module.
*/
function _emitCollectedEvent(
uint256 collectorProfileId,
uint256 publisherProfileId,
uint256 pubId,
uint256 rootProfileId,
uint256 rootPubId,
bytes calldata data
) private {
emit Events.Collected(
collectorProfileId,
publisherProfileId,
pubId,
rootProfileId,
rootPubId,
data,
block.timestamp
);
}
function _follow(
uint256 followerProfileId,
address executor,
uint256 idOfProfileToFollow,
uint256 followTokenId,
bytes calldata followModuleData
) internal returns (uint256) {
uint256 followNFTSlot;
address followModule;
address followNFT;
// Load the follow NFT and follow module for the given profile being followed, and cache
// the follow NFT slot.
assembly {
mstore(0, idOfProfileToFollow)
mstore(32, PROFILE_BY_ID_MAPPING_SLOT)
// The follow NFT offset is 2, the follow module offset is 1,
// so we just need to subtract 1 instead of recalculating the slot.
followNFTSlot := add(keccak256(0, 64), PROFILE_FOLLOW_NFT_OFFSET)
followModule := sload(sub(followNFTSlot, 1))
followNFT := sload(followNFTSlot)
}
if (followNFT == address(0)) {
followNFT = _deployFollowNFT(idOfProfileToFollow);
// Store the follow NFT in the cached slot.
assembly {
sstore(followNFTSlot, followNFT)
}
}
uint256 followTokenIdAssigned = IFollowNFT(followNFT).follow({
followerProfileId: followerProfileId,
executor: executor,
followTokenId: followTokenId
});
if (followModule != address(0)) {
IFollowModule(followModule).processFollow(
followerProfileId,
followTokenId,
executor,
idOfProfileToFollow,
followModuleData
);
}
emit Events.Followed(
followerProfileId,
idOfProfileToFollow,
followTokenIdAssigned,
followModuleData,
block.timestamp
);
return followTokenIdAssigned;
}
/**
* @notice Deploys the given profile's Follow NFT contract.
*
* @param profileId The token ID of the profile which Follow NFT should be deployed.
*
* @return address The address of the deployed Follow NFT contract.
*/
function _deployFollowNFT(uint256 profileId) private returns (address) {
bytes memory functionData = abi.encodeWithSelector(
IFollowNFT.initialize.selector,
profileId
);
address followNFT = address(new FollowNFTProxy(functionData));
emit Events.FollowNFTDeployed(profileId, followNFT, block.timestamp);
return followNFT;
}
function _validateProfileExists(uint256 profileId) private view {
if (GeneralHelpers.unsafeOwnerOf(profileId) == address(0))
revert Errors.TokenDoesNotExist();
}
}

View File

@@ -0,0 +1,592 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {IEIP1271Implementer} from '../../interfaces/IEIP1271Implementer.sol';
import {DataTypes} from '../DataTypes.sol';
import {Errors} from '../Errors.sol';
import {DataTypes} from '../DataTypes.sol';
import {GeneralHelpers} from './GeneralHelpers.sol';
import '../Constants.sol';
/**
* @title MetaTxHelpers
* @author Lens Protocol
*
* @notice This is the library used by the GeneralLib that contains the logic for signature
* validation.
*
* NOTE: the baseFunctions in this contract operate under the assumption that the passed signer is already validated
* to either be the originator or one of their delegated executors.
*
* @dev The functions are internal, so they are inlined into the GeneralLib. User nonces
* are incremented from this library as well.
*/
library MetaTxHelpers {
/// Permit and PermitForAll emit these ERC721 events here an optimization.
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
/**
* @notice Validates parameters and increments the nonce for a given owner using the `permit()`
* function.
*
* @param spender The spender to approve.
* @param tokenId The token ID to approve the spender for.
* @param sig the EIP712Signature struct containing the token owner's signature.
*/
function basePermit(
address spender,
uint256 tokenId,
DataTypes.EIP712Signature calldata sig
) internal {
if (spender == address(0)) revert Errors.ZeroSpender();
address owner = GeneralHelpers.unsafeOwnerOf(tokenId);
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(PERMIT_TYPEHASH, spender, tokenId, _sigNonces(owner), sig.deadline)
)
),
owner,
sig
);
emit Approval(owner, spender, tokenId);
}
function basePermitForAll(
address owner,
address operator,
bool approved,
DataTypes.EIP712Signature calldata sig
) internal {
if (operator == address(0)) revert Errors.ZeroSpender();
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
PERMIT_FOR_ALL_TYPEHASH,
owner,
operator,
approved,
_sigNonces(owner),
sig.deadline
)
)
),
owner,
sig
);
emit ApprovalForAll(owner, operator, approved);
}
function baseSetDefaultProfileWithSig(
address signer,
DataTypes.SetDefaultProfileWithSigData calldata vars
) internal {
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
SET_DEFAULT_PROFILE_WITH_SIG_TYPEHASH,
vars.wallet,
vars.profileId,
_sigNonces(signer),
vars.sig.deadline
)
)
),
signer,
vars.sig
);
}
function baseSetProfileMetadataURIWithSig(
address signer,
DataTypes.SetProfileMetadataURIWithSigData calldata vars
) internal {
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
SET_PROFILE_METADATA_URI_WITH_SIG_TYPEHASH,
vars.profileId,
keccak256(bytes(vars.metadataURI)),
_sigNonces(signer),
vars.sig.deadline
)
)
),
signer,
vars.sig
);
}
function baseSetFollowModuleWithSig(
address signer,
DataTypes.SetFollowModuleWithSigData calldata vars
) internal {
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
SET_FOLLOW_MODULE_WITH_SIG_TYPEHASH,
vars.profileId,
vars.followModule,
keccak256(vars.followModuleInitData),
_sigNonces(signer),
vars.sig.deadline
)
)
),
signer,
vars.sig
);
}
function baseSetDispatcherWithSig(DataTypes.SetDispatcherWithSigData calldata vars) internal {
address owner = GeneralHelpers.unsafeOwnerOf(vars.profileId);
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
SET_DISPATCHER_WITH_SIG_TYPEHASH,
vars.profileId,
vars.dispatcher,
_sigNonces(owner),
vars.sig.deadline
)
)
),
owner,
vars.sig
);
}
function baseSetDelegatedExecutorApprovalWithSig(
DataTypes.SetDelegatedExecutorApprovalWithSigData calldata vars
) internal {
address onBehalfOf = vars.onBehalfOf;
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
SET_DELEGATED_EXECUTOR_APPROVAL_WITH_SIG_TYPEHASH,
vars.onBehalfOf,
vars.executor,
vars.approved,
_sigNonces(onBehalfOf),
vars.sig.deadline
)
)
),
onBehalfOf,
vars.sig
);
}
function baseSetProfileImageURIWithSig(
address signer,
DataTypes.SetProfileImageURIWithSigData calldata vars
) internal {
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
SET_PROFILE_IMAGE_URI_WITH_SIG_TYPEHASH,
vars.profileId,
keccak256(bytes(vars.imageURI)),
_sigNonces(signer),
vars.sig.deadline
)
)
),
signer,
vars.sig
);
}
function baseSetFollowNFTURIWithSig(
address signer,
DataTypes.SetFollowNFTURIWithSigData calldata vars
) internal {
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
SET_FOLLOW_NFT_URI_WITH_SIG_TYPEHASH,
vars.profileId,
keccak256(bytes(vars.followNFTURI)),
_sigNonces(signer),
vars.sig.deadline
)
)
),
signer,
vars.sig
);
}
function basePostWithSig(address signer, DataTypes.PostWithSigData calldata vars) internal {
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
POST_WITH_SIG_TYPEHASH,
vars.profileId,
keccak256(bytes(vars.contentURI)),
vars.collectModule,
keccak256(vars.collectModuleInitData),
vars.referenceModule,
keccak256(vars.referenceModuleInitData),
_sigNonces(signer),
vars.sig.deadline
)
)
),
signer,
vars.sig
);
}
function baseCommentWithSig(address signer, DataTypes.CommentWithSigData calldata vars)
internal
{
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
COMMENT_WITH_SIG_TYPEHASH,
vars.profileId,
keccak256(bytes(vars.contentURI)),
vars.profileIdPointed,
vars.pubIdPointed,
keccak256(vars.referenceModuleData),
vars.collectModule,
keccak256(vars.collectModuleInitData),
vars.referenceModule,
keccak256(vars.referenceModuleInitData),
_sigNonces(signer),
vars.sig.deadline
)
)
),
signer,
vars.sig
);
}
function baseMirrorWithSig(address signer, DataTypes.MirrorWithSigData calldata vars) internal {
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
MIRROR_WITH_SIG_TYPEHASH,
vars.profileId,
vars.profileIdPointed,
vars.pubIdPointed,
keccak256(vars.referenceModuleData),
vars.referenceModule,
keccak256(vars.referenceModuleInitData),
_sigNonces(signer),
vars.sig.deadline
)
)
),
signer,
vars.sig
);
}
function baseBurnWithSig(uint256 tokenId, DataTypes.EIP712Signature calldata sig) internal {
address owner = GeneralHelpers.unsafeOwnerOf(tokenId);
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(BURN_WITH_SIG_TYPEHASH, tokenId, _sigNonces(owner), sig.deadline)
)
),
owner,
sig
);
}
function baseFollowWithSig(address signer, DataTypes.FollowWithSigData calldata vars) internal {
uint256 dataLength = vars.datas.length;
bytes32[] memory dataHashes = new bytes32[](dataLength);
for (uint256 i = 0; i < dataLength; ) {
dataHashes[i] = keccak256(vars.datas[i]);
unchecked {
++i;
}
}
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
FOLLOW_WITH_SIG_TYPEHASH,
vars.followerProfileId,
keccak256(abi.encodePacked(vars.idsOfProfilesToFollow)),
keccak256(abi.encodePacked(vars.followTokenIds)),
keccak256(abi.encodePacked(dataHashes)),
_sigNonces(signer),
vars.sig.deadline
)
)
),
signer,
vars.sig
);
}
function baseUnfollowWithSig(address signer, DataTypes.UnfollowWithSigData calldata vars)
internal
{
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
UNFOLLOW_WITH_SIG_TYPEHASH,
vars.unfollowerProfileId,
keccak256(abi.encodePacked(vars.idsOfProfilesToUnfollow)),
_sigNonces(signer),
vars.sig.deadline
)
)
),
signer,
vars.sig
);
}
function baseSetBlockStatusWithSig(
address signer,
DataTypes.SetBlockStatusWithSigData calldata vars
) internal {
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
SET_BLOCK_STATUS_WITH_SIG_TYPEHASH,
vars.byProfileId,
keccak256(abi.encodePacked(vars.idsOfProfilesToSetBlockStatus)),
keccak256(abi.encodePacked(vars.blockStatus)),
_sigNonces(signer),
vars.sig.deadline
)
)
),
signer,
vars.sig
);
}
function baseCollectWithSig(address signer, DataTypes.CollectWithSigData calldata vars)
internal
{
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
COLLECT_WITH_SIG_TYPEHASH,
vars.collectorProfileId,
vars.publisherProfileId,
vars.pubId,
keccak256(vars.data),
_sigNonces(signer),
vars.sig.deadline
)
)
),
signer,
vars.sig
);
}
function getDomainSeparator() internal view returns (bytes32) {
return _calculateDomainSeparator();
}
/**
* @dev Wrapper for ecrecover to reduce code size, used in meta-tx specific functions.
*/
function _validateRecoveredAddress(
bytes32 digest,
address expectedAddress,
DataTypes.EIP712Signature calldata sig
) internal view {
if (sig.deadline < block.timestamp) revert Errors.SignatureExpired();
// If the expected address is a contract, check the signature there.
if (expectedAddress.code.length != 0) {
bytes memory concatenatedSig = abi.encodePacked(sig.r, sig.s, sig.v);
if (
IEIP1271Implementer(expectedAddress).isValidSignature(digest, concatenatedSig) !=
EIP1271_MAGIC_VALUE
) {
revert Errors.SignatureInvalid();
}
} else {
address recoveredAddress = ecrecover(digest, sig.v, sig.r, sig.s);
if (recoveredAddress == address(0) || recoveredAddress != expectedAddress) {
revert Errors.SignatureInvalid();
}
}
}
// /**
// * @dev Wrapper for ecrecover to reduce code size, used in meta-tx specific functions.
// */
// function _validateRecoveredAddressWithExecutor(
// bytes32 digest,
// address expectedAddress,
// DataTypes.EIP712Signature calldata sig
// ) internal view {
// if (sig.deadline < block.timestamp) revert Errors.SignatureExpired();
// address recoveredAddress = expectedAddress;
// // If the expected address is a contract, check the signature there.
// if (recoveredAddress.code.length != 0) {
// bytes memory concatenatedSig = abi.encodePacked(sig.r, sig.s, sig.v);
// if (
// IEIP1271Implementer(expectedAddress).isValidSignature(digest, concatenatedSig) !=
// EIP1271_MAGIC_VALUE
// ) revert Errors.SignatureInvalid();
// } else {
// recoveredAddress = ecrecover(digest, sig.v, sig.r, sig.s);
// if (recoveredAddress == address(0)) revert Errors.SignatureInvalid();
// // GeneralHelpers.validateOnBehalfOfOrExecutor(expectedAddress, recoveredAddress);
// }
// // Execution passes fine, since either a DE or the expected address is recovered.
// }
// function _validateRecoveredAddressWithExecutorAndReturn(
// bytes32 digest,
// address expectedAddress,
// DataTypes.EIP712Signature calldata sig
// ) internal view returns (address) {
// if (sig.deadline < block.timestamp) revert Errors.SignatureExpired();
// address recoveredAddress = expectedAddress;
// // If the expected address is a contract, check the signature there.
// if (recoveredAddress.code.length != 0) {
// bytes memory concatenatedSig = abi.encodePacked(sig.r, sig.s, sig.v);
// if (
// IEIP1271Implementer(expectedAddress).isValidSignature(digest, concatenatedSig) !=
// EIP1271_MAGIC_VALUE
// ) revert Errors.SignatureInvalid();
// } else {
// recoveredAddress = ecrecover(digest, sig.v, sig.r, sig.s);
// if (recoveredAddress == address(0)) revert Errors.SignatureInvalid();
// // GeneralHelpers.validateOnBehalfOfOrExecutor(expectedAddress, recoveredAddress);
// }
// // Execution passes fine, since either a DE or the expected address is recovered.
// return recoveredAddress;
// }
/**
* @dev Calculates EIP712 DOMAIN_SEPARATOR based on the current contract and chain ID.
*/
function _calculateDomainSeparator() private view returns (bytes32) {
if (block.chainid == POLYGON_CHAIN_ID) {
// Note that this only works on the canonical Polygon mainnet deployment, and should the
// name change, a contract upgrade would be necessary.
return POLYGON_DOMAIN_SEPARATOR;
}
return
keccak256(
abi.encode(
EIP712_DOMAIN_TYPEHASH,
keccak256(_nameBytes()),
EIP712_REVISION_HASH,
block.chainid,
address(this)
)
);
}
/**
* @dev Calculates EIP712 digest based on the current DOMAIN_SEPARATOR.
*
* @param hashedMessage The message hash from which the digest should be calculated.
*
* @return bytes32 A 32-byte output representing the EIP712 digest.
*/
function _calculateDigest(bytes32 hashedMessage) private view returns (bytes32) {
bytes32 digest;
unchecked {
digest = keccak256(
abi.encodePacked('\x19\x01', _calculateDomainSeparator(), hashedMessage)
);
}
return digest;
}
/**
* @dev This fetches a user's signing nonce and increments it, akin to `sigNonces++`.
*
* @param user The user address to fetch and post-increment the signing nonce for.
*
* @return uint256 The signing nonce for the given user prior to being incremented.
*/
function _sigNonces(address user) private returns (uint256) {
uint256 previousValue;
assembly {
mstore(0, user)
mstore(32, SIG_NONCES_MAPPING_SLOT)
let slot := keccak256(0, 64)
previousValue := sload(slot)
sstore(slot, add(previousValue, 1))
}
return previousValue;
}
/**
* @dev Reads the name storage slot and returns the value as a bytes variable.
*
* @return bytes The contract's name.
*/
function _nameBytes() private view returns (bytes memory) {
bytes memory ptr;
assembly {
// Load the free memory pointer, where we'll return the value
ptr := mload(64)
// Load the slot, which either contains the name + 2*length if length < 32 or
// 2*length+1 if length >= 32, and the actual string starts at slot keccak256(NAME_SLOT)
let slotLoad := sload(NAME_SLOT)
let size
// Determine if the length > 32 by checking the lowest order bit, meaning the string
// itself is stored at keccak256(NAME_SLOT)
switch and(slotLoad, 1)
case 0 {
// The name is in the same slot
// Determine the size by dividing the last byte's value by 2
size := shr(1, and(slotLoad, 255))
// Store the size in the first slot
mstore(ptr, size)
// Store the actual string in the second slot (without the size)
mstore(add(ptr, 32), and(slotLoad, not(255)))
}
case 1 {
// The name is not in the same slot
// Determine the size by dividing the value in the whole slot minus 1 by 2
size := shr(1, sub(slotLoad, 1))
// Store the size in the first slot
mstore(ptr, size)
// Compute the total memory slots we need, this is (size + 31) / 32
let totalMemorySlots := shr(5, add(size, 31))
// Iterate through the words in memory and store the string word by word
// prettier-ignore
for { let i := 0 } lt(i, totalMemorySlots) { i := add(i, 1) } {
mstore(add(add(ptr, 32), mul(32, i)), sload(add(NAME_SLOT_GT_31, i)))
}
}
// Store the new memory pointer in the free memory pointer slot
mstore(64, add(add(ptr, 32), size))
}
return ptr;
}
}

View File

@@ -1,8 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {IERC721Time} from '../core/base/IERC721Time.sol';
import {IERC721Time} from '../interfaces/IERC721Time.sol';
import {ILensHub} from '../interfaces/ILensHub.sol';
import {DataTypes} from '../libraries/DataTypes.sol';
import {Events} from '../libraries/Events.sol';
@@ -27,10 +27,6 @@ contract LensPeriphery {
keccak256(
'ToggleFollowWithSig(uint256[] profileIds,bool[] enables,uint256 nonce,uint256 deadline)'
);
bytes32 internal constant SET_PROFILE_METADATA_WITH_SIG_TYPEHASH =
keccak256(
'SetProfileMetadataURIWithSig(uint256 profileId,string metadata,uint256 nonce,uint256 deadline)'
);
ILensHub public immutable HUB;
@@ -42,46 +38,6 @@ contract LensPeriphery {
HUB = hub;
}
/**
* @notice Sets the profile metadata for a given profile.
*
* @param profileId The profile ID to set the metadata for.
* @param metadata The metadata string to set for the profile.
*/
function setProfileMetadataURI(uint256 profileId, string calldata metadata) external {
_validateCallerIsProfileOwnerOrDispatcher(profileId);
_setProfileMetadataURI(profileId, metadata);
}
/**
* @notice Sets the profile metadata for a given profile via signature with the specified parameters.
*
* @param vars A SetProfileMetadataWithSigData struct containingthe regular parameters and an EIP712Signature struct.
*/
function setProfileMetadataURIWithSig(DataTypes.SetProfileMetadataWithSigData calldata vars)
external
{
unchecked {
address owner = IERC721Time(address(HUB)).ownerOf(vars.profileId);
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
SET_PROFILE_METADATA_WITH_SIG_TYPEHASH,
vars.profileId,
keccak256(bytes(vars.metadata)),
sigNonces[owner]++,
vars.sig.deadline
)
)
),
owner,
vars.sig
);
}
_setProfileMetadataURI(vars.profileId, vars.metadata);
}
/**
* @notice Toggle Follows on the given profiles, emiting toggle event for each FollowNFT.
*
@@ -122,22 +78,6 @@ contract LensPeriphery {
_toggleFollow(vars.follower, vars.profileIds, vars.enables);
}
/**
* @notice Returns the metadata URI of a profile.
*
* @param profileId The profile ID to query the metadata URI for.
*
* @return string The metadata associated with that profile ID, or an empty string if it is not set or the profile does not exist.
*/
function getProfileMetadataURI(uint256 profileId) external view returns (string memory) {
return _metadataByProfile[profileId];
}
function _setProfileMetadataURI(uint256 profileId, string calldata metadata) internal {
_metadataByProfile[profileId] = metadata;
emit Events.ProfileMetadataSet(profileId, metadata, block.timestamp);
}
function _toggleFollow(
address follower,
uint256[] calldata profileIds,
@@ -164,11 +104,13 @@ contract LensPeriphery {
) {
return;
}
revert Errors.NotProfileOwnerOrDispatcher();
revert Errors.NotProfileOwnerOrValid();
}
/**
* @dev Wrapper for ecrecover to reduce code size, used in meta-tx specific functions.
*
* @notice In order to use the MetaTXHelpers here, we will need to re-deploy.
*/
function _validateRecoveredAddress(
bytes32 digest,

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {ILensHub} from '../interfaces/ILensHub.sol';
import {DataTypes} from '../libraries/DataTypes.sol';
@@ -23,21 +23,6 @@ contract ProfileCreationProxy is Ownable {
}
function proxyCreateProfile(DataTypes.CreateProfileData memory vars) external onlyOwner {
uint256 handleLength = bytes(vars.handle).length;
if (handleLength < 5) revert Errors.HandleLengthInvalid();
bytes1 firstByte = bytes(vars.handle)[0];
if (firstByte == '-' || firstByte == '_' || firstByte == '.')
revert Errors.HandleFirstCharInvalid();
for (uint256 i = 1; i < handleLength; ) {
if (bytes(vars.handle)[i] == '.') revert Errors.HandleContainsInvalidCharacters();
unchecked {
++i;
}
}
vars.handle = string(abi.encodePacked(vars.handle, '.lens'));
LENS_HUB.createProfile(vars);
}
}

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {ILensHub} from '../interfaces/ILensHub.sol';
import {DataTypes} from '../libraries/DataTypes.sol';
@@ -42,19 +42,4 @@ contract UIDataProvider {
uint256 pubCount = profileStruct.pubCount;
return LatestData(profileStruct, HUB.getPub(profileId, pubCount));
}
/**
* @notice Returns the profile struct and latest publication struct associated with the passed
* profile ID.
*
* @param handle The handle to query.
*
* @return LensData A struct containing the `ProfileStruct` and the `PublicationStruct` queried.
*/
function getLatestDataByHandle(string memory handle) external view returns (LatestData memory) {
uint256 profileId = HUB.getProfileIdByHandle(handle);
DataTypes.ProfileStruct memory profileStruct = HUB.getProfile(profileId);
uint256 pubCount = profileStruct.pubCount;
return LatestData(profileStruct, HUB.getPub(profileId, pubCount));
}
}

View File

@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {IEIP1271Implementer} from '../interfaces/IEIP1271Implementer.sol';
/**
* @dev This is a mock contract that always returns the wrong value upon being checked with EIP-1271.
*/
contract BadMockEIP1271Implementer is IEIP1271Implementer {
function isValidSignature(bytes32 _hash, bytes memory _signature)
external
view
override
returns (bytes4)
{
return bytes4(0xFFFFFFFF);
}
}

View File

@@ -1,9 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol';
/**
* @dev A simple mock currency to be used for testing.
*/
contract Currency is ERC20('Currency', 'CRNC') {
function mint(address to, uint256 amount) external {
_mint(to, amount);

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {IFollowNFT} from '../interfaces/IFollowNFT.sol';
@@ -17,16 +17,4 @@ contract Helper {
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);
}
}

View File

@@ -0,0 +1,43 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {ICollectModule} from '../interfaces/ICollectModule.sol';
/**
* @title FreeCollectModule
* @author Lens Protocol
*
* @notice This is a simple Lens CollectModule implementation, inheriting from the ICollectModule interface.
*
* This module works by allowing all collects.
*/
contract MockCollectModule is ICollectModule {
/**
* @dev There is nothing needed at initialization.
*/
function initializePublicationCollectModule(
uint256,
address,
uint256,
bytes calldata data
) external pure override returns (bytes memory) {
uint256 number = abi.decode(data, (uint256));
require(number == 1, 'MockCollectModule: invalid');
return new bytes(0);
}
/**
* @dev Processes a collect by:
* 1. Ensuring the collector is a follower, if needed
*/
function processCollect(
uint256,
uint256,
address collector,
address,
uint256 profileId,
uint256 pubId,
bytes calldata
) external view override {}
}

View File

@@ -0,0 +1,40 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {IDeprecatedCollectModule} from '../interfaces/IDeprecatedCollectModule.sol';
/**
* @title FreeCollectModule
* @author Lens Protocol
*
* @notice This is a simple Lens CollectModule implementation, inheriting from the ICollectModule interface.
*
* This module works by allowing all collects.
*/
contract MockDeprecatedCollectModule is IDeprecatedCollectModule {
/**
* @dev There is nothing needed at initialization.
*/
function initializePublicationCollectModule(
uint256,
uint256,
bytes calldata data
) external pure override returns (bytes memory) {
uint256 number = abi.decode(data, (uint256));
require(number == 1, 'MockReferenceModule: invalid');
return new bytes(0);
}
/**
* @dev Processes a collect by:
* 1. Ensuring the collector is a follower, if needed
*/
function processCollect(
uint256,
address collector,
uint256 profileId,
uint256 pubId,
bytes calldata
) external view override {}
}

View File

@@ -0,0 +1,40 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {IDeprecatedFollowModule} from '../interfaces/IDeprecatedFollowModule.sol';
/**
* @dev This is a simple mock follow module to be used for testing.
*/
contract MockDeprecatedFollowModule is IDeprecatedFollowModule {
function initializeFollowModule(
uint256,
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 isFollowing(
uint256,
address,
uint256
) external pure override returns (bool) {
return true;
}
function followModuleTransferHook(
uint256 profileId,
address from,
address to,
uint256 followNFTTokenId
) external override {}
}

View File

@@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {IDeprecatedReferenceModule} from '../interfaces/IDeprecatedReferenceModule.sol';
/**
* @dev This is a simple mock follow module to be used for testing.
*/
contract MockDeprecatedReferenceModule is IDeprecatedReferenceModule {
function initializeReferenceModule(
uint256,
uint256,
bytes calldata data
) external pure override returns (bytes memory) {
uint256 number = abi.decode(data, (uint256));
require(number == 1, 'MockDeprecatedReferenceModule: invalid');
return new bytes(0);
}
function processComment(
uint256 profileId,
uint256 profileIdPointed,
uint256 pubIdPointed,
bytes calldata data
) external override {}
function processMirror(
uint256 profileId,
uint256 profileIdPointed,
uint256 pubIdPointed,
bytes calldata data
) external override {}
}

View File

@@ -0,0 +1,47 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {IEIP1271Implementer} from '../interfaces/IEIP1271Implementer.sol';
/**
* @dev This is a mock contract that validates an EIP1271 signature against its deployer.
*/
contract MockEIP1271Implementer is IEIP1271Implementer {
// bytes4(keccak256("isValidSignature(bytes32,bytes)")
bytes4 internal constant MAGIC_VALUE = 0x1626ba7e;
address public immutable OWNER;
constructor() {
OWNER = msg.sender;
}
function isValidSignature(bytes32 _hash, bytes memory _signature)
external
view
override
returns (bytes4)
{
require(_signature.length == 65, 'Invalid signature length');
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := mload(add(_signature, 32))
s := mload(add(_signature, 64))
v := shr(248, mload(add(_signature, 96)))
}
// (bytes32 r, bytes32 s, uint8 v) = abi.decode(_signature, (bytes32, bytes32, uint8));
address signer = ecrecover(
keccak256(abi.encodePacked('\x19Ethereum Signed Message:\n32', _hash)),
v,
r,
s
);
require(signer != address(0), 'Invalid recovery');
return signer == OWNER ? MAGIC_VALUE : bytes4(0xFFFFFFFF);
}
}

View File

@@ -1,39 +1,28 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {IFollowModule} from '../interfaces/IFollowModule.sol';
/**
* @dev This is a simple mock follow module to be used for testing.
*/
contract MockFollowModule is IFollowModule {
function initializeFollowModule(uint256 profileId, bytes calldata data)
external
pure
override
returns (bytes memory)
{
function initializeFollowModule(
uint256 profileId,
address executor,
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 followerProfileId,
uint256 followTokenId,
address executor,
uint256 profileId,
bytes calldata data
) external override {}
function isFollowing(
uint256 profileId,
address follower,
uint256 followNFTTokenId
) external view override returns (bool) {
return true;
}
function followModuleTransferHook(
uint256 profileId,
address from,
address to,
uint256 followNFTTokenId
) external override {}
) external pure override {}
}

View File

@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import {IFollowModule} from '../interfaces/IFollowModule.sol';
/**
* @dev This is a simple mock follow module to be used for testing revert cases on processFollow.
*/
contract MockFollowModuleWithRevertFlag is IFollowModule {
error MockFollowModuleReverted();
function initializeFollowModule(
uint256 profileId,
address executor,
bytes calldata data
) external pure override returns (bytes memory) {
return new bytes(0);
}
function processFollow(
uint256 followerProfileId,
uint256 followTokenId,
address executor,
uint256 profileId,
bytes calldata data
) external pure override {
if (abi.decode(data, (bool))) {
revert MockFollowModuleReverted();
}
}
}

View File

@@ -1,14 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
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';

View File

@@ -1,14 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
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';

View File

@@ -1,68 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {DataTypes} from '../libraries/DataTypes.sol';
import {LensHubStorage} from '../core/storage/LensHubStorage.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;
mapping(address => uint256) internal _defaultProfileByAddress;
uint256 internal _profileCounter;
address internal _governance;
address internal _emergencyAdmin;
/**
* @dev This is a simple mock LensHub storage contract to be used for testing.
*/
contract MockLensHubV2Storage is LensHubStorage {
uint256 internal _additionalValue;
}

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {ILensHub} from '../interfaces/ILensHub.sol';
import {DataTypes} from '../libraries/DataTypes.sol';
@@ -20,21 +20,6 @@ contract MockProfileCreationProxy {
}
function proxyCreateProfile(DataTypes.CreateProfileData memory vars) external {
uint256 handleLength = bytes(vars.handle).length;
if (handleLength < 5) revert Errors.HandleLengthInvalid();
bytes1 firstByte = bytes(vars.handle)[0];
if (firstByte == '-' || firstByte == '_' || firstByte == '.')
revert Errors.HandleFirstCharInvalid();
for (uint256 i = 1; i < handleLength; ) {
if (bytes(vars.handle)[i] == '.') revert Errors.HandleContainsInvalidCharacters();
unchecked {
++i;
}
}
vars.handle = string(abi.encodePacked(vars.handle, '.test'));
LENS_HUB.createProfile(vars);
}
}

View File

@@ -1,13 +1,17 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {IReferenceModule} from '../interfaces/IReferenceModule.sol';
/**
* @dev This is a simple mock follow module to be used for testing.
*/
contract MockReferenceModule is IReferenceModule {
function initializeReferenceModule(
uint256 profileId,
uint256 pubId,
uint256,
address,
uint256,
bytes calldata data
) external pure override returns (bytes memory) {
uint256 number = abi.decode(data, (uint256));
@@ -17,6 +21,7 @@ contract MockReferenceModule is IReferenceModule {
function processComment(
uint256 profileId,
address executor,
uint256 profileIdPointed,
uint256 pubIdPointed,
bytes calldata data
@@ -24,6 +29,7 @@ contract MockReferenceModule is IReferenceModule {
function processMirror(
uint256 profileId,
address executor,
uint256 profileIdPointed,
uint256 pubIdPointed,
bytes calldata data

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {ILensHub} from '../interfaces/ILensHub.sol';
import {Proxy} from '@openzeppelin/contracts/proxy/Proxy.sol';

View File

@@ -3,7 +3,7 @@
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import '@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol';
/**
* NOTE: This is a direct copy of OpenZeppelin's TransparentUpgradeableProxy and is only present for
@@ -39,7 +39,7 @@ contract TransparentUpgradeableProxy is ERC1967Proxy {
address admin_,
bytes memory _data
) payable ERC1967Proxy(_logic, _data) {
assert(_ADMIN_SLOT == bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1));
assert(_ADMIN_SLOT == bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1));
_changeAdmin(admin_);
}
@@ -97,7 +97,7 @@ contract TransparentUpgradeableProxy is ERC1967Proxy {
* NOTE: Only the admin can call this function. See {ProxyAdmin-upgrade}.
*/
function upgradeTo(address newImplementation) external ifAdmin {
_upgradeToAndCall(newImplementation, bytes(""), false);
_upgradeToAndCall(newImplementation, bytes(''), false);
}
/**
@@ -107,7 +107,11 @@ contract TransparentUpgradeableProxy is ERC1967Proxy {
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-upgradeAndCall}.
*/
function upgradeToAndCall(address newImplementation, bytes calldata data) external payable ifAdmin {
function upgradeToAndCall(address newImplementation, bytes calldata data)
external
payable
ifAdmin
{
_upgradeToAndCall(newImplementation, data, true);
}
@@ -122,7 +126,10 @@ contract TransparentUpgradeableProxy is ERC1967Proxy {
* @dev Makes sure the admin cannot access the fallback function. See {Proxy-_beforeFallback}.
*/
function _beforeFallback() internal virtual override {
require(msg.sender != _getAdmin(), "TransparentUpgradeableProxy: admin cannot fallback to proxy target");
require(
msg.sender != _getAdmin(),
'TransparentUpgradeableProxy: admin cannot fallback to proxy target'
);
super._beforeFallback();
}
}

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
pragma solidity 0.8.15;
import {Errors} from '../libraries/Errors.sol';

View File

@@ -1,22 +0,0 @@
version: '3.5'
services:
contracts-env:
user: "${USERID?}:${USERID?}"
env_file:
- .env
build:
context: ./
stdin_open: true
tty: true
volumes:
- ./:/src:rw
- $HOME/.tenderly/config.yaml:/root/.tenderly/config.yaml:ro
#environment:
#- MNEMONIC=
#- RPC_URL=
#- BLOCK_EXPLORER_KEY=
#- TENDERLY_PROJECT=
#- TENDERLY_USERNAME=
#- TENDERLY_FORK_ID=
#- TENDERLY_HEAD_ID=

View File

@@ -1,3 +0,0 @@
#!/bin/bash
[ ! -d "/src/node_modules" ] && cp /node_modules /src/node_modules; bash

View File

@@ -0,0 +1,38 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import 'forge-std/Script.sol';
import 'forge-std/console2.sol';
import '../contracts/core/LensHub.sol';
import '../contracts/core/FollowNFT.sol';
import '../contracts/core/CollectNFT.sol';
/**
* This script will deploy the current repository implementations, using the given environment
* hub proxy address.
*/
contract DeployUpgradeScript is Script {
function run() public {
uint256 deployerKey = vm.envUint('DEPLOYER_KEY');
address deployer = vm.addr(deployerKey);
address hubProxyAddr = vm.envAddress('HUB_PROXY_ADDRESS');
// Start deployments.
vm.startBroadcast(deployerKey);
// Precompute needed addresss.
address followNFTAddr = computeCreateAddress(deployer, 1);
address collectNFTAddr = computeCreateAddress(deployer, 2);
// Deploy implementation contracts.
address hubImpl = address(new LensHub(followNFTAddr, collectNFTAddr));
address followNFT = address(new FollowNFT(hubProxyAddr));
address collectNFT = address(new CollectNFT(hubProxyAddr));
vm.writeFile("addrs", "");
vm.writeLine("addrs", string(abi.encodePacked("hubImpl: ", vm.toString(hubImpl))));
vm.writeLine("addrs", string(abi.encodePacked("followNFT: ", vm.toString(followNFT))));
vm.writeLine("addrs", string(abi.encodePacked("collectNFT: ", vm.toString(collectNFT))));
}
}

13
foundry.toml Normal file
View File

@@ -0,0 +1,13 @@
[profile.default]
src = 'contracts'
out = 'out'
libs = ['node_modules', 'lib']
test = 'test/foundry'
cache_path = 'forge-cache'
fs_permissions = [{ access = "read-write", path = "./"}]
[rpc_endpoints]
polygon = "${POLYGON_RPC_URL}"
[fuzz]
runs = 10000

View File

@@ -16,6 +16,7 @@ import 'hardhat-gas-reporter';
import 'hardhat-contract-sizer';
import 'hardhat-log-remover';
import 'hardhat-spdx-license-identifier';
import 'hardhat-tracer';
if (!process.env.SKIP_LOAD) {
glob.sync('./tasks/**/*.ts').forEach(function (file) {
@@ -23,7 +24,6 @@ if (!process.env.SKIP_LOAD) {
});
}
const DEFAULT_BLOCK_GAS_LIMIT = 12450000;
const MNEMONIC_PATH = "m/44'/60'/0'/0";
const MNEMONIC = process.env.MNEMONIC || '';
const MAINNET_FORK = process.env.MAINNET_FORK === 'true';
@@ -48,10 +48,13 @@ const mainnetFork = MAINNET_FORK
: undefined;
const config: HardhatUserConfig = {
tracer: {
enabled: false,
},
solidity: {
compilers: [
{
version: '0.8.10',
version: '0.8.15',
settings: {
optimizer: {
enabled: true,
@@ -73,11 +76,6 @@ const config: HardhatUserConfig = {
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 }) => ({
@@ -85,6 +83,7 @@ const config: HardhatUserConfig = {
balance,
})),
forking: mainnetFork,
allowUnlimitedContractSize: true,
},
},
gasReporter: {

View File

@@ -0,0 +1,27 @@
name: Tests
on: [push, pull_request]
jobs:
check:
name: Foundry project
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
submodules: recursive
- name: Install Foundry
uses: onbjerg/foundry-toolchain@v1
with:
version: nightly
- name: Install dependencies
run: forge install
- name: Run tests
run: forge test -vvv
- name: Build Test with older solc versions
run: |
forge build --contracts src/Test.sol --use solc:0.8.0
forge build --contracts src/Test.sol --use solc:0.7.6
forge build --contracts src/Test.sol --use solc:0.7.0
forge build --contracts src/Test.sol --use solc:0.6.0

Some files were not shown because too many files have changed in this diff Show More