Platform: Code4rena
Start Date: 17/07/2023
Pot Size: $85,500 USDC
Total HM: 11
Participants: 26
Period: 14 days
Judge: Picodes
Total Solo HM: 1
Id: 263
League: ETH
Rank: 2/26
Findings: 6
Award: $12,925.99
🌟 Selected for report: 2
🚀 Solo Findings: 0
2660.5978 USDC - $2,660.60
https://github.com/code-423n4/2023-07-lens/blob/main/contracts/LensHub.sol#L368-L373 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/FollowNFT.sol#L131-L138 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/misc/LensV2Migration.sol#L33 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/misc/LensV2Migration.sol#L37-L41 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/misc/LensV2Migration.sol#L45 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/base/LensProfiles.sol#L93-L98 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/base/LensProfiles.sol#L165-L169 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/FollowNFT.sol#L156 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/FollowNFT.sol#L164 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/FollowNFT.sol#L188 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/FollowNFT.sol#L255 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/FollowNFT.sol#L436-L440
Important user functions on Lens V2 are protected by pause modifiers like whenNotPaused
, or whenPublishingEnabled
.
This includes functions to follow, unfollow profiles, set follow modules, or create profiles among others.
Several important functions that modify contract storage by users, or perform actions on tokens are not protected by any pause modifiers. This allows users to interact with the contracts, although they should not be supposed to.
The most important ones being:
FollowNFT::removeFollower()
despite LensHub::unfollow
is protected with whenNotPaused
LensHub
via:
LensV2Migration::batchMigrateProfiles()
LensV2Migration::batchMigrateFollows()
LensV2Migration::batchMigrateFollowModules()
In addition, some other FollowNFT
functions could be considered for pause modifiers, as LensHub
tokens have similar protections on the inherited LensProfiles
contract:
FollowNFT::wrap()
FollowNFT::unwrap()
FollowNFT::burn()
FollowNFT::_beforeTokenTransfer()
Unfollow actions are prevented on paused contracts for LensHub::unfollow()
, but can still be executed via FollowNFT::removeFollower()
:
function unfollow(uint256 unfollowerProfileId, uint256[] calldata idsOfProfilesToUnfollow) external override whenNotPaused onlyProfileOwnerOrDelegatedExecutor(msg.sender, unfollowerProfileId) {
function removeFollower(uint256 followTokenId) external override { address followTokenOwner = ownerOf(followTokenId); if (followTokenOwner == msg.sender || isApprovedForAll(followTokenOwner, msg.sender)) { _unfollowIfHasFollower(followTokenId); } else { revert DoesNotHavePermissions(); } }
LensV2Migration
is inherited by LensHub
and has the following functions missing any pause modifiers:
function batchMigrateProfiles(uint256[] calldata profileIds) external { function batchMigrateFollows( uint256[] calldata followerProfileIds, uint256[] calldata idsOfProfileFollowed, uint256[] calldata followTokenIds ) external { function batchMigrateFollowModules(uint256[] calldata profileIds) external {
LensProfiles
is inherited by LensHub
and protects certain functions regarding the token like these ones:
function burn(uint256 tokenId) public override(LensBaseERC721, IERC721Burnable) @> whenNotPaused onlyProfileOwner(msg.sender, tokenId) { function _beforeTokenTransfer( address from, address to, uint256 tokenId @> ) internal override whenNotPaused {
On the other hand, FollowNFT
has no pause protection on functions related to its token. Such as:
function wrap(uint256 followTokenId, address wrappedTokenReceiver) external override { function wrap(uint256 followTokenId) external override { function unwrap(uint256 followTokenId) external override { function burn(uint256 followTokenId) public override { function _beforeTokenTransfer( address from, address to, uint256 followTokenId ) internal override {
Manual Review
Add the corresponding pause modifiers to the functions mentioned on the Impact section.
Other
#0 - c4-pre-sort
2023-08-04T12:41:36Z
141345 marked the issue as primary issue
#1 - vicnaum
2023-08-07T15:55:03Z
We confirm the issue, although this might be rather considered as Low instead of Medium
#2 - c4-sponsor
2023-08-07T15:55:08Z
vicnaum marked the issue as disagree with severity
#3 - c4-judge
2023-08-28T13:42:47Z
Picodes marked the issue as satisfactory
#4 - Picodes
2023-08-28T14:10:13Z
Downgrading to Low as assets nor availability of the protocol is at risk
#5 - c4-judge
2023-08-28T14:10:22Z
Picodes changed the severity to QA (Quality Assurance)
#6 - 0xJuancito
2023-08-29T03:42:13Z
Hi @Picodes. I'd like to ask if you could take a second look at this issue as a Medium risk finding, considering the Docs:
2 — Med: Assets not at direct risk, but the function of the protocol or its availability could be impacted
In this case the "function of the protocol" is impacted, and availability can also be considered as well. Not for being unavailable, but because of functions being available in moments that they shouldn't be, allowing important actions for a social network that users should not be able to perform, out of the control of the protocol.
When the protocol is paused, it should not allow unfollow actions (via removeFollower()
) or new follows (via batchMigrateProfiles()
for example, among all the other mentioned functions on the Impact section.
In DeFi protocols, missing pause modifiers having been evaluated as Medium like here, and here. In the case of Lens Protocol, the functions mentioned on the Impact section should be considered of ultimate importance to have under control as a social network.
#7 - MiloTruck
2023-08-30T02:10:44Z
Hi @Picodes, just wanted to mention that #144 is a duplicate of this issue in case it is upgraded. Thanks!
#8 - donosonaumczuk
2023-08-30T16:15:42Z
Yeah, I think it is a fair argument and can be upgraded to Medium.
#9 - Picodes
2023-08-31T16:29:12Z
My view on this is that it ultimately depends on the sponsor's intent. In this case, it seems clear by the above comment and what you highlighted that the intent was to be able to totally pause follows and unfollows, so you're right and I'll upgrade this to Med as a functionality is broken
#10 - c4-judge
2023-08-31T16:29:34Z
This previously downgraded issue has been upgraded by Picodes
#11 - c4-judge
2023-08-31T16:33:04Z
Picodes marked issue #144 as primary and marked this issue as a duplicate of 144
1596.3587 USDC - $1,596.36
https://github.com/code-423n4/2023-07-lens/blob/main/contracts/namespaces/LensHandles.sol#L87-L99 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/namespaces/LensHandles.sol#L194-L200
There is not on-chain check that prevents users from minting handles on v2 that already exist on v1. This would prevent OG users of v1 from migrating their handles.
In order to stay true to the spirit of a decentralized social network, this check should be on-chain, and not depend on any possible off-chain list that the whitelisted profile creators, or the owner may have, as it could incomplete, with errors, or may not exist (as it is not mentioned).
Example 1: In the case a whitelisted profile creator does not keep an up to date list of the v1 profiles that should not be able to be minted, an adversary may choose a valuable Lens Handle name that hasn't been migrated.
Example 2: The contract owner can unknowingly provide a Lens Handle of an OG v1 user to another one.
This can't be undone without any upgrade.
Lens Handles represent part of the core user's identity on Lens Protocol v1. Users that own a Lens Handle on v1 should be expected to to migrate their handles to v2.
If other user mints their handle before they migrate it to v2, they will lose their right to own their corresponding handle on the new version of Lens.
The impact is permanent and can't be undone without any upgrade.
LensHandles::mintHandle()
does not make any on-chain check to see if the v1 handle exists. If this function is called before migrateHandle()
, the OG v1 user won't be able to migrate their handle:
function mintHandle(address to, string calldata localName) external onlyOwnerOrWhitelistedProfileCreator returns (uint256) { _validateLocalName(localName); return _mintHandle(to, localName); } function migrateHandle(address to, string calldata localName) external onlyHub returns (uint256) { _validateLocalNameMigration(localName); return _mintHandle(to, localName); } function _mintHandle(address to, string calldata localName) internal returns (uint256) { uint256 tokenId = getTokenId(localName); _mint(to, tokenId); _localNames[tokenId] = localName; emit HandlesEvents.HandleMinted(localName, NAMESPACE, tokenId, to, block.timestamp); return tokenId; }
Manual Review
Check for the existence of the corresponding Lens Handle on v1 before calling to mint it with the new mintHandle()
function.
Invalid Validation
#0 - c4-pre-sort
2023-08-04T12:33:09Z
141345 marked the issue as duplicate of #143
#1 - c4-judge
2023-08-28T15:57:21Z
Picodes marked the issue as satisfactory
2660.5978 USDC - $2,660.60
https://github.com/code-423n4/2023-07-lens/blob/main/contracts/libraries/constants/Typehash.sol#L23 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/libraries/constants/Typehash.sol#L15 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/libraries/constants/Typehash.sol#L25 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/libraries/MetaTxLib.sol#L147-L148 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/libraries/MetaTxLib.sol#L245-L246 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/libraries/MetaTxLib.sol#L275-L276 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/libraries/constants/Types.sol#L181-L182 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/libraries/constants/Types.sol#L210-L211 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/libraries/constants/Types.sol#L239-L240
Signed transactions implementing EIP-712 signatures correctly will revert as their on-chain counterpart will be different.
Affected functions:
LensHub::postWithSig()
LensHub::commentWithSig()
LensHub::quoteWithSig()
The functions postWithSig()
, commentWithSig()
, and quoteWithSig()
from LensHub
all use EIP-712 to validate the owner/executor signature.
The contract implements incorrectly the Typehash for the EIP-712 signature, which will not be able to validate correct signatures, thus reverting those transactions.
The error is on the Typehashes assuming address collectModule
, and bytes collectModuleInitData
, instead of the corresponding address[] actionModules
, and bytes[] actionModulesInitDatas
, which have different types.
Note the use of address collectModule
, and bytes collectModuleInitData
on the Typehashes:
bytes32 constant POST = keccak256('Post(uint256 profileId,string contentURI,address collectModule,bytes collectModuleInitData,address referenceModule,bytes referenceModuleInitData,uint256 nonce,uint256 deadline)'); bytes32 constant COMMENT = keccak256('Comment(uint256 profileId,string contentURI,uint256 pointedProfileId,uint256 pointedPubId,uint256[] referrerProfileIds,uint256[] referrerPubIds,bytes referenceModuleData,address collectModule,bytes collectModuleInitData,address referenceModule,bytes referenceModuleInitData,uint256 nonce,uint256 deadline)'); bytes32 constant QUOTE = keccak256('Quote(uint256 profileId,string contentURI,uint256 pointedProfileId,uint256 pointedPubId,uint256[] referrerProfileIds,uint256[] referrerPubIds,bytes referenceModuleData,address collectModule,bytes collectModuleInitData,address referenceModule,bytes referenceModuleInitData,uint256 nonce,uint256 deadline)');
These are the functions where the corresponding digests are calculated:
function validatePostSignature(Types.EIP712Signature calldata signature, Types.PostParams calldata postParams) // @audit check external { _validateRecoveredAddress( _calculateDigest( keccak256( abi.encode( Typehash.POST, postParams.profileId, keccak256(bytes(postParams.contentURI)), @> postParams.actionModules, // @audit address[] actionModules @> _hashActionModulesInitDatas(postParams.actionModulesInitDatas), // @audit bytes[] actionModulesInitDatas postParams.referenceModule, keccak256(postParams.referenceModuleInitData), _getAndIncrementNonce(signature.signer), signature.deadline ) ) ), signature ); } function validateCommentSignature( Types.EIP712Signature calldata signature, Types.CommentParams calldata commentParams ) external { bytes32 contentURIHash = keccak256(bytes(commentParams.contentURI)); bytes32 referenceModuleDataHash = keccak256(commentParams.referenceModuleData); bytes32 actionModulesInitDataHash = _hashActionModulesInitDatas(commentParams.actionModulesInitDatas); bytes32 referenceModuleInitDataHash = keccak256(commentParams.referenceModuleInitData); uint256 nonce = _getAndIncrementNonce(signature.signer); uint256 deadline = signature.deadline; bytes memory encodedAbi = _abiEncode( ReferenceParamsForAbiEncode( Typehash.COMMENT, commentParams.profileId, contentURIHash, commentParams.pointedProfileId, commentParams.pointedPubId, commentParams.referrerProfileIds, commentParams.referrerPubIds, referenceModuleDataHash, @> commentParams.actionModules, // @audit address[] actionModules @> actionModulesInitDataHash, // @audit bytes[] actionModulesInitDatas commentParams.referenceModule, referenceModuleInitDataHash, nonce, deadline ) ); _validateRecoveredAddress(_calculateDigest(keccak256(encodedAbi)), signature); } function validateQuoteSignature(Types.EIP712Signature calldata signature, Types.QuoteParams calldata quoteParams) external { bytes32 contentURIHash = keccak256(bytes(quoteParams.contentURI)); bytes32 referenceModuleDataHash = keccak256(quoteParams.referenceModuleData); bytes32 actionModulesInitDataHash = _hashActionModulesInitDatas(quoteParams.actionModulesInitDatas); bytes32 referenceModuleInitDataHash = keccak256(quoteParams.referenceModuleInitData); uint256 nonce = _getAndIncrementNonce(signature.signer); uint256 deadline = signature.deadline; bytes memory encodedAbi = _abiEncode( ReferenceParamsForAbiEncode( Typehash.QUOTE, quoteParams.profileId, contentURIHash, quoteParams.pointedProfileId, quoteParams.pointedPubId, quoteParams.referrerProfileIds, quoteParams.referrerPubIds, referenceModuleDataHash, @> quoteParams.actionModules, // @audit address[] actionModules @> actionModulesInitDataHash, // @audit bytes[] actionModulesInitDatas quoteParams.referenceModule, referenceModuleInitDataHash, nonce, deadline ) ); _validateRecoveredAddress(_calculateDigest(keccak256(encodedAbi)), signature); }
These are the corresponding type declarations:
struct PostParams { uint256 profileId; string contentURI; @> address[] actionModules; @> bytes[] actionModulesInitDatas; address referenceModule; bytes referenceModuleInitData; } struct CommentParams { uint256 profileId; string contentURI; uint256 pointedProfileId; uint256 pointedPubId; uint256[] referrerProfileIds; uint256[] referrerPubIds; bytes referenceModuleData; @> address[] actionModules; @> bytes[] actionModulesInitDatas; address referenceModule; bytes referenceModuleInitData; } struct QuoteParams { uint256 profileId; string contentURI; uint256 pointedProfileId; uint256 pointedPubId; uint256[] referrerProfileIds; uint256[] referrerPubIds; bytes referenceModuleData; @> address[] actionModules; @> bytes[] actionModulesInitDatas; address referenceModule; bytes referenceModuleInitData; }
Manual Review
Fix the Typehashes for COMMENT
, POST
, and QUOTE
:
address collectModule
-> address[] actionModules
bytes collectModuleInitData
-> bytes[] actionModulesInitDatas
Other
#0 - donosonaumczuk
2023-08-08T15:13:25Z
This is a duplicate or subset from #141 - We proceed with the same resolution.
#1 - c4-sponsor
2023-08-08T15:13:30Z
donosonaumczuk marked the issue as disagree with severity
#2 - c4-judge
2023-08-28T13:41:23Z
Picodes marked the issue as duplicate of #141
#3 - c4-judge
2023-08-28T21:00:19Z
Picodes marked the issue as satisfactory
2075.2663 USDC - $2,075.27
https://github.com/code-423n4/2023-07-lens/blob/main/contracts/libraries/FollowLib.sol#L35-L37 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/FollowNFT.sol#L480-L520
Users are not supposed to be able to self-follow on Lens v2, but they are able to bypass the restriction. This can also affect modules or newer functionalities that count on this behaviour.
Migration is an Area of specific concern for the devs, and this can easily be prevented with a simple check.
This can't be undone without any upgrade.
FollowLib::follow()
has a specific restriction to revert when a user tries to self-follow on Lens v2:
if (followerProfileId == idsOfProfilesToFollow[i]) { revert Errors.SelfFollow(); }
However, users that own a follow NFT from V1 can execute FollowNFT::tryMigrate()
to self-follow on V2, as there is no restriction to prevent it. A test proving it can be found on the next section.
Add this test to test/migrations/Migrations.t.sol
and run TESTING_FORK=mainnet POLYGON_RPC_URL="https://polygon.llamarpc.com" forge test --mt "testSelfFollow"
.
Note: In case of a memory allocation error during the Forge test, please comment these lines. They are not used for the current test.
function testSelfFollow() public onlyFork { uint256 selfFollowProfileId = 3659; // juancito.lens uint256 selfFollowTokenId = 42; // juancito.lens follows juancito.lens on V1 FollowNFT nft = FollowNFT(hub.getProfile(selfFollowProfileId).followNFT); address user = nft.ownerOf(selfFollowTokenId); // Owner of juancito.lens // 1. Migrate the self-follow uint256[] memory selfFollowProfileIdArray = new uint256[](1); uint256[] memory selfFollowTokenIdArray = new uint256[](1); selfFollowProfileIdArray[0] = selfFollowProfileId; // 3659 selfFollowTokenIdArray[0] = selfFollowTokenId; // 42 hub.batchMigrateFollows(selfFollowProfileIdArray, selfFollowProfileIdArray, selfFollowTokenIdArray); // 2. The user is self-following on V2 assertTrue(nft.isFollowing(selfFollowProfileId)); }
Manual Review
Add the following validation to FollowNFT::tryMigrate()
:
+ if (followerProfileId == _followedProfileId) { + return 0; + }
Invalid Validation
#0 - c4-pre-sort
2023-08-04T10:56:59Z
141345 marked the issue as primary issue
#1 - vicnaum
2023-08-07T16:05:35Z
This seems like a sub-case of this: https://github.com/code-423n4/2023-07-lens-findings/issues/112 (But the mitigation is different for this case)
#2 - c4-sponsor
2023-08-08T17:26:27Z
vicnaum marked the issue as sponsor confirmed
#3 - Picodes
2023-08-28T16:08:44Z
The impact is the same but the issue is it seems different, as the mitigation suggested by #112 wouldn't prevent this from happening
#4 - c4-judge
2023-08-28T16:08:55Z
Picodes marked the issue as satisfactory
#5 - c4-judge
2023-08-28T16:10:34Z
Picodes marked the issue as selected for report
3458.7772 USDC - $3,458.78
https://github.com/code-423n4/2023-07-lens/blob/main/contracts/FollowNFT.sol#L35-L37 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/FollowNFT.sol#L216-L218 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/FollowNFT.sol#L386-L391 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/FollowNFT.sol#L514 https://github.com/code-423n4/2023-07-lens/blob/main/contracts/FollowNFT.sol#L501-L504
Follows in Lens v2 can be checked via isFollowing()
, which returns the internal storage variable _followTokenIdByFollowerProfileId[followerProfileId]
and can't be manipulated by an unauthorized party via any of the follow/unfollow functions, as they have proper access control checks.
This is also prevented on token transfers in Lens v2. If user A wraps their follow token and transfer it to user B, it doesn't mean that user B is following user A.
But using the tryMigrate()
it is possible to "force" someone to follow another user without their consent.
Users can make any other user follow them.
For a social network this could even be considered a high severity issue, as follow actions are a core component of them, and migrations were marked as an Area of specific concern by the devs.
Unauthorized follow actions not only harm the user, by performing a crutial action without their consent, but may also affect other users.
It could be used for example to make prestigious profiles follow scam accounts, unwillingly ligitimating them.
Extra note: An adversary can execute this attack at any time. They can prevent having their follow NFT being migrated by frontrunning the migrate tx, and moving their token to another wallet. The migration will run silently with no error, and the attack can be performed later.
Follows can be checked by the isFollowing()
function, which is dependant on the _followTokenIdByFollowerProfileId[followerProfileId]
mapping. _followTokenIdByFollowerProfileId
is unset for Lens v1:
// Introduced in v2 mapping(uint256 => uint256) internal _followTokenIdByFollowerProfileId; function isFollowing(uint256 followerProfileId) external view override returns (bool) { return _followTokenIdByFollowerProfileId[followerProfileId] != 0; }
The expected way to update _followTokenIdByFollowerProfileId
is via the _baseFollow()
internal function, which can only be called via other external functions protected with access control, only authorizing the profile owner who will follow or a delegate:
function _baseFollow( uint256 followerProfileId, uint256 followTokenId, bool isOriginalFollow ) internal { _followTokenIdByFollowerProfileId[followerProfileId] = followTokenId;
However the tryMigrate()
function also updates _followDataByFollowTokenId
to help with migrations from V1, and can be called by anyone (via the hub):
function tryMigrate( uint256 followerProfileId, address followerProfileOwner, uint256 idOfProfileFollowed, uint256 followTokenId ) external onlyHub returns (uint48) { // ... _followDataByFollowTokenId[followTokenId].followerProfileId = uint160(followerProfileId);
The only caveat is that the "ProfileNFT and FollowNFT should be in the same account":
address followTokenOwner = ownerOf(followTokenId); // ProfileNFT and FollowNFT should be in the same account if (followerProfileOwner != followTokenOwner) { return 0; // Not holding both Profile & Follow NFTs together }
But this can bypassed by simply transfering a V1 follow NFT from anyone to the victim, who will end up following the adversary unwillingly after the execution of tryMigrate()
.
Also, as noted on the Impact section, an adversary could prevent that someone else tries to migrate their profile, by frontrunning the tx and moving their token to some other address. It will "fail" silently because of the return 0; // Not holding both Profile & Follow NFTs together
on the previous code snippet.
Add this test to test/migrations/Migrations.t.sol
and run TESTING_FORK=mainnet POLYGON_RPC_URL="https://polygon.llamarpc.com" forge test --mt "testFakeFollowMigration"
.
Note: In case of a memory allocation error during the Forge test, please comment these lines. They are not used for the current test.
function testFakeFollowMigration() public onlyFork { // lensprotocol.lens will be the victim // juancito.lens will be the adversary // The adversary will make lensprotocol.lens follow them without their consent uint256 victimProfileId = 1; // lensprotocol.lens uint256 adversaryProfileId = 3659; // juancito.lens uint256 followTokenId = 42; // juancito.lens follows juancito.lens FollowNFT nft = FollowNFT(hub.getProfile(adversaryProfileId).followNFT); address adversary = nft.ownerOf(followTokenId); // Owner of juancito.lens // 1. Transfer the Lens v1 follow token to the victim // This does not automatically imply that they are following the adversary yet vm.startPrank(address(adversary)); nft.transferFrom(address(adversary), hub.ownerOf(victimProfileId), followTokenId); assertFalse(nft.isFollowing(victimProfileId)); // 2. Migrate the fake follow. Anyone can run this uint256[] memory victimProfileIdArray = new uint256[](1); uint256[] memory adversaryProfileIdArray = new uint256[](1); uint256[] memory followTokenIdArray = new uint256[](1); victimProfileIdArray[0] = victimProfileId; // 1 adversaryProfileIdArray[0] = adversaryProfileId; // 3659 followTokenIdArray[0] = followTokenId; // 42 hub.batchMigrateFollows(victimProfileIdArray, adversaryProfileIdArray, followTokenIdArray); // 3. The victim is now following the adversary assertTrue(nft.isFollowing(victimProfileId)); }
Manual Review
The follow migration should be reworked to account for the case of "fake" follows. Some ideas are to allow the bulk process to be run by whitelisted accounts, or by the specific token owners. A combination of both may also work, by only migrating tokens that have been minted but not transfered, for example. Or letting users opt-in for an automatic migration to give some ideas.
Additionally log an event with the reason the migration was not executed for a specific profile.
Invalid Validation
#0 - donosonaumczuk
2023-08-08T16:10:33Z
Duplicated from #112 and #106
#1 - c4-sponsor
2023-08-08T17:26:18Z
vicnaum marked the issue as sponsor confirmed
#2 - c4-judge
2023-08-28T16:05:44Z
Picodes marked the issue as duplicate of #112
#3 - c4-judge
2023-08-28T16:05:59Z
Picodes marked the issue as selected for report
#4 - c4-judge
2023-08-28T16:06:06Z
Picodes marked the issue as satisfactory
🌟 Selected for report: MiloTruck
Also found by: 0xAnah, AlexCzm, Bughunter101, BugzyVonBuggernaut, DavidGiladi, Emmanuel, Iurii3, Kaysoft, MohammedRizwan, Prestige, Rolezn, Sathish9098, Stormreckson, adeolu, descharre, evmboi32, fatherOfBlocks, ginlee, ihtishamsudo, juancito, mrudenko, tnquanghuy0512
474.3819 USDC - $474.38
On the LensProfiles
contract, the functions enableTokenGuardian()
and DANGER__disableTokenGuardian()
have a modifier onlyEOA
to only allow EOAs to execute them. The onlyEOA
modifier uses OpenZeppelin library to check msg.sender.isContract()
.
This function only checks that the contract code is zero, which is also true during the constructor phase.
A contract may acquire a token on its constructor
function, enable the guardian and continue with its initialization (code length will be zero, so it will pass the check).
The token will be stuck on the contract.
The reason is that the _beforeTokenTransfer()
will revert and the guardian can't be disabled. This is because the onlyEOA
is applied to DANGER__disableTokenGuardian
:
24: import {Address} from '@openzeppelin/contracts/utils/Address.sol'; 27: using Address for address; 50: modifier onlyEOA() { 51: if (msg.sender.isContract()) { 52: revert Errors.NotEOA(); 53: } 54: _; 55: } 63: function DANGER__disableTokenGuardian() external onlyEOA { 77: function enableTokenGuardian() external onlyEOA { 165: function _beforeTokenTransfer( 166: address from, 167: address to, 168: uint256 tokenId 169: ) internal override whenNotPaused { 170: if (from != address(0) && _hasTokenGuardianEnabled(from)) { 171: // Cannot transfer profile if the guardian is enabled, except at minting time. 172: revert Errors.GuardianEnabled(); 173: }
Allow anyone to disable their token guardian regardless of their account type. The DANGER__disableTokenGuardian()
function already checks that the token was previously enabled, so this won't affect any other current behaviour.
unlink()
can be called for any unlinked handles by providing tokenId = 0
TokenHandleRegistry::unlink()
can be called with any unlinked handleId
, and tokenId = 0
.
This is because handleToToken[_handleHash(handle)]
will returned an empty struct, tokenPointedByHandle.id
will be zero, and the if statement if (tokenPointedByHandle.id != tokenId) { revert RegistryErrors.NotLinked(); }
will not enter the condition.
An event will still be emitted in _unlink()
.
Note: tokenId = 0
is not a valid token value, as it starts on 1.
function unlink(uint256 handleId, uint256 tokenId) external { // We revert here only in the case if both tokens exists and the caller is not the owner of any of them if ( ILensHandles(LENS_HANDLES).exists(handleId) && ILensHandles(LENS_HANDLES).ownerOf(handleId) != msg.sender && ILensHub(LENS_HUB).exists(tokenId) && ILensHub(LENS_HUB).ownerOf(tokenId) != msg.sender ) { revert RegistryErrors.NotHandleNorTokenOwner(); } RegistryTypes.Handle memory handle = RegistryTypes.Handle({collection: LENS_HANDLES, id: handleId}); RegistryTypes.Token memory tokenPointedByHandle = handleToToken[_handleHash(handle)]; // We check if the tokens are (were) linked for the case if some of them doesn't exist @> if (tokenPointedByHandle.id != tokenId) { // @audit-info tokenId == 0 for unlinked handles revert RegistryErrors.NotLinked(); } _unlink(handle, tokenPointedByHandle); } function _unlink(RegistryTypes.Handle memory handle, RegistryTypes.Token memory token) internal { delete handleToToken[_handleHash(handle)]; // tokenToHandle is removed too, as the first version linkage is one-to-one. delete tokenToHandle[_tokenHash(token)]; @> emit RegistryEvents.HandleUnlinked(handle, token, block.timestamp); }
TokenHandleRegistry.sol#L72-L91 TokenHandleRegistry.sol#L164
Revert if tokenPointedByHandle.id
is 0, as it will indicate that the handle is not linked.
Profile Image URI has a max size of 6000 as defined via MAX_PROFILE_IMAGE_URI_LENGTH = 6000;
, but the Metadata URI doesn't have any related validation. This can arise the same problems with large URLs as with the profile image.
Apply a validation for max length to Metadata URI as well.
Lens Handles has a MAX_HANDLE_LENGTH
, but it is lacking a MIN_HANDLE_LENGTH
to prevent users from claiming short handles.
Currently on Lens V1, handles with a length shorter than 5 characters can't be minted.
Implement a min handle length for Lens Handles, not dependant on whitelisted registrators.
#0 - c4-judge
2023-08-28T20:38:24Z
Picodes marked the issue as grade-a