GithubHelp home page GithubHelp logo

2023-07-lens-findings's Introduction

Lens Protocol V2 Audit

Unless otherwise discussed, this repo will be made public after audit completion, sponsor review, judging, and issue mitigation window.

Contributors to this repo: prior to report publication, please review the Agreements & Disclosures issue.


Audit findings are submitted to this repo

Sponsors have three critical tasks in the audit process:

  1. Weigh in on severity.
  2. Respond to issues.
  3. Share your mitigation of findings.

Let's walk through each of these.

High and Medium Risk Issues

Please note: because wardens submit issues without seeing each other's submissions, there will always be findings that are duplicates. For all issues labeled 3 (High Risk) or 2 (Medium Risk), these have been pre-sorted for you so that there is only one primary issue open per unique finding. All duplicates have been labeled duplicate, linked to a primary issue, and closed.

Weigh in on severity

Judges have the ultimate discretion in determining severity of issues, as well as whether/how issues are considered duplicates. However, sponsor input is a significant criteria.

For a detailed breakdown of severity criteria and how to estimate risk, please refer to the judging criteria in our documentation.

If you disagree with a finding's severity, leave the severity label intact and add the label disagree with severity, along with a comment indicating your opinion for the judges to review. You may also add questions for the judge in the comments.

Respond to issues

Label each open/primary High or Medium risk finding as one of these:

  • sponsor confirmed, meaning: "Yes, this is a problem and we intend to fix it."
  • sponsor disputed, meaning either: "We cannot duplicate this issue" or "We disagree that this is an issue at all."
  • sponsor acknowledged, meaning: "Yes, technically the issue is correct, but we are not going to resolve it for xyz reasons."

(Note: please don't use sponsor disputed for a finding if you think it should be considered of lower or higher severity. Instead, use the label disagree with severity and add comments to recommend a different severity level -- and include your reasoning.)

Add any necessary comments explaining your rationale for your evaluation of the issue.

Note that when the repo is public, after all issues are mitigated, wardens will read these comments; they may also be included in your C4 audit report.

QA and Gas Reports

For low and non-critical findings (AKA QA), as well as gas optimizations: wardens are required to submit these as bulk listings of issues and recommendations. They may only submit a single, compiled report in each category:

  • QA reports should include all low severity and non-critical findings, along with a summary statement.
  • Gas reports should include all gas optimization recommendations, along with a summary statement.

For QA and Gas reports, sponsors are not required to weigh in on severity or risk level. We ask that sponsors:

  • Leave a comment for the judge on any reports you consider to be particularly high quality. (These reports will be awarded on a curve.)
  • Add the sponsor disputed label to any reports that you think should be completely disregarded by the judge, i.e. the report contains no valid findings at all.

Once labelling is complete

When you have finished labelling findings, drop the C4 team a note in your private Discord backroom channel and let us know you've completed the sponsor review process. At this point, we will pass the repo over to the judge to review your feedback while you work on mitigations.

Share your mitigation of findings

Note: this section does not need to be completed in order to finalize judging. You can continue work on mitigations while the judge finalizes their decisions and even beyond that. Ultimately we won't publish the final audit report until you give us the OK.

For each finding you have confirmed, you will want to mitigate the issue before the contest report is made public.

If you are planning a Code4rena mitigation review:

  1. In your own Github repo, create a branch based off of the commit you used for your Code4rena audit, then
  2. Create a separate Pull Request for each High or Medium risk C4 audit finding (e.g. one PR for finding H-01, another for H-02, etc.)
  3. Link the PR to the issue that it resolves within your contest findings repo.

Most C4 mitigation reviews focus exclusively on reviewing mitigations of High and Medium risk findings. Therefore, QA and Gas mitigations should be done in a separate branch. If you want your mitigation review to include QA or Gas-related PRs, please reach out to C4 staff and let’s chat!

If several findings are inextricably related (e.g. two potential exploits of the same underlying issue, etc.), you may create a single PR for the related findings.

If you aren’t planning a mitigation review

  1. Within a repo in your own GitHub organization, create a pull request for each finding.
  2. Link the PR to the issue that it resolves within your contest findings repo.

This will allow for complete transparency in showing the work of mitigating the issues found in the contest. If the issue in question has duplicates, please link to your PR from the open/primary issue.

2023-07-lens-findings's People

Contributors

code423n4 avatar c4-judge avatar itsmetechjay avatar

Watchers

Ashok avatar

2023-07-lens-findings's Issues

QA Report

See the markdown file with the details of this report here.

Royalty percentage is hardcoded

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/main/contracts/FollowNFT.sol#L55

Vulnerability details

Impact

Royalty is hardcoded which breaks the basic architecture of Lens Protocol

Proof of Concept

As per the docs it is written that royalty can be set by the owner of the handle but _setRoyalty() is hardcoded to 10% which breaks the basic architecture of the Lens protocol.

https://github.com/code-423n4/2023-07-lens/blob/main/contracts/FollowNFT.sol#L55

Tools Used

Manual review

Recommended Mitigation Steps

Use a method like setter function to set the royalty.

Assessed type

Context

Agreements & Disclosures

Agreements

If you are a C4 Certified Contributor by commenting or interacting with this repo prior to public release of the contest report, you agree that you have read the Certified Warden docs and agree to be bound by:

To signal your agreement to these terms, add a 👍 emoji to this issue.

Code4rena staff reserves the right to disqualify anyone from this role and similar future opportunities who is unable to participate within the above guidelines.

Disclosures

Sponsors may elect to add team members and contractors to assist in sponsor review and triage. All sponsor representatives added to the repo should comment on this issue to identify themselves.

To ensure contest integrity, the following potential conflicts of interest should also be disclosed with a comment in this issue:

  1. any sponsor staff or sponsor contractors who are also participating as wardens
  2. any wardens hired to assist with sponsor review (and thus presenting sponsor viewpoint on findings)
  3. any wardens who have a relationship with a judge that would typically fall in the category of potential conflict of interest (family, employer, business partner, etc)
  4. any other case where someone might reasonably infer a possible conflict of interest.

possible DOS of batchMigrateProfiles() and batchMigrateFollows() in MigrateLib.sol

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/libraries/MigrationLib.sol#L37
https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/libraries/MigrationLib.sol#L94

Vulnerability details

Impact

batchMigrateProfiles() and batchMigrateFollows() in MigrateLib.sol are external functions callable by anyone and perfom loop functions in which the number of runs is determined by externally supplied args to the function. Since they do not have a mechanism to stop, it’s only based on the length of their supplied argument, it can be very long may take all the gas limit. If the gas limit is reached, this transaction will fail or revert.

Proof of Concept

https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/libraries/MigrationLib.sol#L94-L112

    function batchMigrateFollows(
        uint256[] calldata followerProfileIds,
        uint256[] calldata idsOfProfileFollowed,
        uint256[] calldata followTokenIds
    ) external {
        if (
            followerProfileIds.length != idsOfProfileFollowed.length ||
            followerProfileIds.length != followTokenIds.length
        ) {
            revert Errors.ArrayMismatch();
        }
        uint256 i;
        while (i < followerProfileIds.length) {
            _migrateFollow(followerProfileIds[i], idsOfProfileFollowed[i], followTokenIds[i]);
            unchecked {
                ++i;
            }
        }
    }

https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/libraries/MigrationLib.sol#L37-L49

    function batchMigrateProfiles(
        uint256[] calldata profileIds,
        LensHandles lensHandles,
        TokenHandleRegistry tokenHandleRegistry
    ) external {
        uint256 i;
        while (i < profileIds.length) {
            _migrateProfile(profileIds[i], lensHandles, tokenHandleRegistry);
            unchecked {
                ++i;
            }
        }
    }

Tools Used

VS CODE

Recommended Mitigation Steps

add reasonable limit to the max number of elements that can be in the arrays supplied

Assessed type

Loop

Calls _mint Instead of _safeMint

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/main/contracts/LensHub.sol#L98

Vulnerability details

#Note:
I reported issue that was overlooked by the winning bot.

Calls _mint

  • Severity: Medium
  • Confidence: High

Description

The _mint function is often used to create new tokens in a Solidity smart contract.However, if the function is not implemented correctly, it can introduce vulnerabilities such as integer overflow and underflow, reentrancy, and other types of attacks.To address these issues, the safeMint function was introduced as part of the OpenZeppelin library.The safeMint function includes additional checks to prevent potential attacks, making it a safer alternative to _mint.

There are 1 instances of this issue:

  • File: contracts/LensHub.sol
 
Line: 98          _mint(createProfileParams.to, profileId)

use safeMint instead.

https://github.com/code-423n4/2023-07-lens/blob/main/contracts/LensHub.sol#L98

Assessed type

ERC721

QA Report

See the markdown file with the details of this report here.

signature.signer can be a hackable contract that will return the right value for validation

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/libraries/MetaTxLib.sol#L469-L483

Vulnerability details

Impact

2023-07-lens\contracts\libraries\MetaTxLib.sol

In the function _validateRecoveredAddress(bytes32 digest, Types.EIP712Signature calldata signature), you can fake a validation result if (IERC1271(signature.signer).isValidSignature(digest, concatenatedSig) != EIP1271_MAGIC_VALUE) {...}. If signature.signer is a hacker contract that returns isValidSignature(...) == EIP1271_MAGIC_VALUE.

And then all MetaTxLib functions will be hacked, with any random signature, in which signature.signer is a hacker contract.

Here are the functions of MetaTxLib.sol affected by this error:

  • validateSetProfileMetadataURISignature,
  • validateSetFollowModuleSignature,
  • validateChangeDelegatedExecutorsConfigSignature,
  • validateSetProfileImageURISignature,
  • validatePostSignature,
  • validateCommentSignature,
  • validateQuoteSignature,
  • validateMirrorSignature,
  • validateBurnSignature,
  • validateFollowSignature,
  • validateUnfollowSignature,
  • validateSetBlockStatusSignature,
  • validateLegacyCollectSignature,
  • validateActSignature

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

In this POC, I have modified the _validateRecoveredAddress function so that it returns true if the result is executed and there is no revert(). The logic of the function remains the same as it was originally.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol';

contract MetaTxLib {
    bytes4 constant EIP1271_MAGIC_VALUE = 0x1626ba7e;

    struct EIP712Signature {
            address signer;
            uint8 v;
            bytes32 r;
            bytes32 s;
            uint256 deadline;
        }

    function _validateRecoveredAddress(bytes32 digest, EIP712Signature calldata signature) public view returns(bool){
        if (signature.deadline < block.timestamp) revert("Errors.SignatureExpired()");
        // If the expected address is a contract, check the signature there.
        if (signature.signer.code.length != 0) {
            bytes memory concatenatedSig = abi.encodePacked(signature.r, signature.s, signature.v);
            if (IERC1271(signature.signer).isValidSignature(digest, concatenatedSig) != EIP1271_MAGIC_VALUE) {
                revert("Errors.SignatureInvalid()");
            }
        } else {
            address recoveredAddress = ecrecover(digest, signature.v, signature.r, signature.s);
            if (recoveredAddress == address(0) || recoveredAddress != signature.signer) {
                revert("Errors.SignatureInvalid()");
            }
        }
        // will return true only if the transaction is not reverted
        return true;
    }
}

FakeERC1271 is our signature.signer, which will return the value we need for signature validation

contract FakeERC1271 {
    function isValidSignature(bytes32 hash, bytes memory signature) external view returns (bytes4 magicValue) {
        magicValue = 0x1626ba7e;
    }
}

HackValidation is a contract that performs a function in MetaTxLib, with our dummy data signature.signer. signature.r, signature.s, signature.v - can be any values.

function validate(...) will return true if the validation has been cracked. in any other cases, revert will occur

contract HackValidation {
    address public signer; // address of FakeERC1271 contract
    uint8 public v = 27; // random
    bytes32 public r = 0x223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20e; // random
    bytes32 public s = 0x2aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663; // random
    uint256 public deadline = 99999999999999999999; // > block.timestamp 

    MetaTxLib public metaTxLib; // address of MetaTxLib contract

    constructor (address metaTxLib_, address fakeERC1271_) {
        signer = fakeERC1271_; // address of FakeERC1271 contract
        metaTxLib = MetaTxLib(metaTxLib_); // address of MetaTxLib contract
    }

    function validate(bytes32 digest) public view returns(bool) {
        MetaTxLib.EIP712Signature memory signature = MetaTxLib.EIP712Signature(signer, v, r, s, deadline);
        return metaTxLib._validateRecoveredAddress(digest, signature);
    }
}

Tools Used

Remix for POC, VS Code

Recommended Mitigation Steps

Modify the function so that signature.signer is an argument to the function.
Change the validation function from isValidSignature(digest, concatenatedSig) to isValidSignature(signature.signer, digest, concatenatedSig)

Assessed type

Invalid Validation

```LensHandles._hasTokenGuardianEnabled(address)``` uses a dangerous strict equality i.e block.stamp

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/namespaces/LensHandles.sol#L256

Vulnerability details

Impact

Use of strict equalities that can be easily manipulated by an attacker.

Proof of Concept

function _hasTokenGuardianEnabled(address wallet) internal view returns (bool) {
        return
            !wallet.isContract() &&
            (_tokenGuardianDisablingTimestamp[wallet] == 0 ||
                block.timestamp < _tokenGuardianDisablingTimestamp[wallet]);
    }

A potential vulnerability is that block.timestamp is manipulatable by miners to a certain degree, hence in certain edge cases where there's a close time difference between block.timestamp and _tokenGuardianDisablingTimestamp[wallet], a miner could potentially manipulate the result of this function.

Tools Used

Static Code Analyzer

Recommended Mitigation Steps

Use block numbers instead of timestamps for time-sensitive conditions as they are not manipulable by miners.

Assessed type

Other

QA Report

See the markdown file with the details of this report here.

Short Address Attack Vulnerability in validateChangeDelegatedExecutorsConfigSignature Function

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/4ad521f84c43771d365ec0b35f904158201943fb/contracts/libraries/MetaTxLib.sol#L87

Vulnerability details

Impact

The validateChangeDelegatedExecutorsConfigSignature function in the MetaTxLib contract is vulnerable to a Short Address Attack. This vulnerability arises due to the use of abi.encodePacked for encoding delegatedExecutors and approvals arrays. abi.encodePacked does not encode the length or the type of the data, which can lead to a mismatch in the array lengths.

If exploited, this vulnerability could lead to unauthorized changes in the executor configuration for delegators in the contract. A malicious user could potentially pass in a larger array of delegatedExecutors and a smaller array of approvals to manipulate the approvals of executors beyond their intended range.

Proof of Concept

Here's how the vulnerability could be exploited:

  • A malicious actor constructs two arrays: delegatedExecutors and approvals. The delegatedExecutors array is made longer than the approvals array.
  • The actor then calls the validateChangeDelegatedExecutorsConfigSignature function with these arrays. The function uses abi.encodePacked to encode the two arrays. Since abi.encodePacked does not encode the length of the arrays, it cannot detect the mismatch in their lengths.
  • As the approvals array is shorter, there won't be a corresponding approval for the extra delegated executors. However, due to the way abi.encodePacked works, the function will not revert or error out. This mismatch allows the actor to manipulate the approvals for executors beyond their intended range.

The root of this vulnerability is the use of abi.encodePacked for encoding arrays. A safer approach would be to use abi.encode, which encodes the length and type of data, thereby preventing this type of attack.

Recommended Mitigation Steps

The vulnerability can be mitigated by replacing abi.encodePacked with abi.encode for encoding delegatedExecutors and approvals. The abi.encode function includes the data type and length in the encoding, which prevents the potential for a Short Address Attack.

function validateChangeDelegatedExecutorsConfigSignature(
    Types.EIP712Signature calldata signature,
    uint256 delegatorProfileId,
    address[] calldata delegatedExecutors,
    bool[] calldata approvals,
    uint64 configNumber,
    bool switchToGivenConfig
) external {
    uint256 nonce = _getAndIncrementNonce(signature.signer);
    uint256 deadline = signature.deadline;
    _validateRecoveredAddress(
        _calculateDigest(
            keccak256(
                abi.encode(
                    Typehash.CHANGE_DELEGATED_EXECUTORS_CONFIG,
                    delegatorProfileId,
                    abi.encode(delegatedExecutors), // Use abi.encode instead of abi.encodePacked
                    abi.encode(approvals), // Use abi.encode instead of abi.encodePacked
                    configNumber,
                    switchToGivenConfig,
                    nonce,
                    deadline
                )
            )
        ),
        signature
    );
}

Assessed type

Other

QA Report

See the markdown file with the details of this report here.

Business Logic Flaw in Follow/Unfollow Functionality in LensV2Follow Contract

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/4ad521f84c43771d365ec0b35f904158201943fb/contracts/LensHub.sol#L368

Vulnerability details

Impact

Looking closely at the LensV2Follow contract, I think I've stumbled upon something interesting. It seems like there's a logic flaw in how the follow/unfollow system is implemented which could potentially allow some crafty users to artificially increase follower counts. Let's dig in!

When a user (say, User A) opts to unfollow another user (User B), the token associated with User A's 'follow' action transitions into an "unwrapped" state. Simultaneously, User A is granted the ability to recover this token. This is effectively handled by the unfollow function:

 if (followTokenOwner == address(0)) {
        // Follow token is unwrapped.
        // Unfollowing and allowing recovery.
        _unfollow({unfollower: unfollowerProfileId, followTokenId: followTokenId});
        _followDataByFollowTokenId[followTokenId].profileIdAllowedToRecover = unfollowerProfileId;
    } 

Now, here's the catch. If User A decides to re-follow User B using the same token before recovering it, the system permits this action. The profileIdAllowedToRecover is not reset after a successful re-follow operation, which is evident in the _followByRecoveringToken function:

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;
}

Consequently, User A can keep unfollowing and following User B using the same token (without ever recovering it), leading to an artificial increase in User B's follower count.

The impact of this issue lies in the potential misrepresentation of a user's popularity on the Lens platform. Even though this doesn't cause any direct monetary loss, it could skew the user experience and the system's overall integrity.

Proof of Concept

  • User A follows User B, creating a Follow token.
  • User A unfollows User B. The Follow token becomes unwrapped, and User A is allowed to recover it.
  • Instead of recovering the Follow token, User A follows User B again using the same token.
  • The profileIdAllowedToRecover is not reset after the follow operation, so User A is able to unfollow and follow User B again without ever recovering the token.
  • By repeating steps 2-4, User A can artificially inflate the follower count of User B.

Recommended Mitigation Steps

To mitigate this issue, the profileIdAllowedToRecover field should be reset after a successful follow operation in the _followByRecoveringToken function. This would prevent a user from being able to follow again with the same token without first recovering it.

function _followByRecoveringToken(uint256 followerProfileId, uint256 followTokenId) internal returns (uint256) {
    if (_followDataByFollowTokenId[followTokenId].profileIdAllowedToRecover != followerProfileId) {
        revert FollowTokenDoesNotExist();
    }
    ...
    _baseFollow({followerProfileId: followerProfileId, followTokenId: followTokenId, isOriginalFollow: false});
    
    // Reset profileIdAllowedToRecover to default value
    _followDataByFollowTokenId[followTokenId].profileIdAllowedToRecover = 0;

    return followTokenId;
}

In this version of _followByRecoveringToken, the profileIdAllowedToRecover is reset to a default value (0 in this case) after the follow operation has been successfully completed. This change ensures that a user can't follow again with the same token without first recovering it, thus preventing the potential inflation of follower counts.

Assessed type

Other

Unauthorized Minting of Wrapped Follow Tokens

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/FollowNFT.sol#L156

Vulnerability details

Impact

The contract allows unauthorized minting of wrapped follow tokens. When a user wants to follow someone, they can wrap their follow token, which results in the creation of a new NFT that represents the follow relationship. However, this minting process does not perform any permission checks, allowing attackers to mint wrapped follow tokens on behalf of other profiles without proper authorization.

Proof of Concept

Validate the Vulnerability:
- An attacker deploys a smart contract that interacts with the FollowNFT contract and implements the IFollowNFT interface.
- The attacker calls the wrap function on their contract, passing the followTokenId of the target profile they want to follow and their own Ethereum address as the wrappedTokenReceiver.
- The FollowNFT contract mints a new follow token and assigns it to the attacker's Ethereum address, representing the follow relationship between the attacker and the target profile.

This problem allows an attacker to impersonate others and mint wrapped follow tokens without their consent, potentially leading to unauthorized access to exclusive content or privileges associated with the follow relationship.

Tools Used

MANUAL REVIEW

Recommended Mitigation Steps

Add permission checks to the minting process. This would ensure that only authorized users can mint wrapped follow tokens.

Assessed type

Other

Attacker was able to transfer token even with Guardian enabled

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/main/contracts/base/LensProfiles.sol#L165

Vulnerability details

The _beforeTokenTransfer function provided does not correctly enforce the guardian restrictions on token transfers. The issue lies in the guardian-enabled check, which is only applied to non-minting transfers, but not to minting transfers.

Real function:

function _beforeTokenTransfer(
    address from,
    address to,
    uint256 tokenId
) internal override whenNotPaused {
    if (from != address(0) && _hasTokenGuardianEnabled(from)) {
        // Cannot transfer profile if the guardian is enabled, except at minting time.
        revert Errors.GuardianEnabled();
    }
    // Switches to new fresh delegated executors configuration (except on minting, as it already has a fresh setup).
    if (from != address(0)) {
        ProfileLib.switchToNewFreshDelegatedExecutorsConfig(tokenId);
    }
    super._beforeTokenTransfer(from, to, tokenId);
}

Explanation of the Issue:

The _beforeTokenTransfer function allows token transfers during minting (i.e., when from == address(0)), even if the guardian is enabled for the recipient (to) address. This means an attacker can mint a new token with a guardian-enabled profile as the recipient and later initiate token transfers without the guardian check being enforced.

Attacker Steps:

  1. The attacker mints a new token with a guardian-enabled profile as the recipient (to) address.

  2. After the minting is successful, the attacker calls the token transfer function to transfer the newly minted token to another address.

  3. The _beforeTokenTransfer function is called, but since from == address(0) (during minting), the guardian-enabled check is bypassed, and the token transfer is allowed, even if the recipient has a guardian enabled.

Recommendation:

To address this issue and enforce the guardian restrictions correctly, the _beforeTokenTransfer function should apply the guardian-enabled check to all token transfers, including during minting. You can modify the function to include the guardian check for all transfers as follows:

function _beforeTokenTransfer(
    address from,
    address to,
    uint256 tokenId
) internal override whenNotPaused {
    if (from != address(0) && _hasTokenGuardianEnabled(from)) {
        // Cannot transfer profile if the guardian is enabled.
        revert Errors.GuardianEnabled();
    }
    // Switches to new fresh delegated executors configuration (except on minting, as it already has a fresh setup).
    if (from != address(0)) {
        ProfileLib.switchToNewFreshDelegatedExecutorsConfig(tokenId);
    }
    super._beforeTokenTransfer(from, to, tokenId);
}

By making this change, the _beforeTokenTransfer function will correctly enforce the guardian restrictions on all token transfers, including minting transfers, preventing the attacker from bypassing the guardian check during token transfers.

Assessed type

Access Control

Case Insensitivity in _isAlphaNumeric Function

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/namespaces/LensHandles.sol#L250

Vulnerability details

Impact

Severity: Medium
Likelihood: High

Description

The _isAlphaNumeric function in the LensHandles.sol contract does not correctly handle uppercase letters. This function is intended to check if a given string is alphanumeric, but due to the current implementation, it fails to recognize uppercase letters as valid alphanumeric characters. This could lead to unexpected behavior if the function is used to validate user inputs or other strings that may contain uppercase letters.

Tools Used

Manual Code Review

Recommendation

It is recommended to adjust the _isAlphaNumeric function to correctly handle uppercase letters. This could be achieved by converting the input string to lowercase before performing the alphanumeric check, or by adjusting the check itself to include the range of ASCII values for uppercase letters.

Recommended Mitigation Steps

  • Modify the _isAlphaNumeric function to include uppercase letters in its validation. This could be done by adding an additional condition to check for the ASCII values of uppercase letters. Specifically, the function could be updated as follows:
return (char >= '0' && char <= '9') || (char >= 'a' && char <= 'z') ||  (char >= 'A' && char <= 'Z');

  • After making the necessary changes, thoroughly test the function with a variety of inputs to ensure it behaves as expected. This should include strings with uppercase letters, lowercase letters, numbers, and non-alphanumeric characters.
  • Consider adding comments to the function to clearly document its intended behavior and any assumptions it makes about its inputs. This can help prevent similar issues in the future.

Assessed type

Invalid Validation

QA Report

See the markdown file with the details of this report here.

[M-01] Dangerous use of uninitialized storage variables

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/5103b29e71ad0e93cbad1f555291698fa4d6676e/contracts/libraries/PublicationLib.sol#L277

Vulnerability details

Impact

M-01 Dangerous use of uninitialized storage variables

The uninitialized storage variable will contain data stored in memory which can be accessed via writing and executing a function that simply calls the value that was last in memory for Types.Publication. This will allow the attacker to manipulate the Types.Publication output and view values being passed through Types.Publication.

Bug

The vulnerable code on line 277 is depicted as follow

        Types.Publication storage _referencePub;

Proof of Concept

URL of vulnerable code line

https://github.com/code-423n4/2023-07-lens/blob/5103b29e71ad0e93cbad1f555291698fa4d6676e/contracts/libraries/PublicationLib.sol#L277

POC

 function typesPublicationFxAttack() external view {
        uint256 _referencePub;
        _referencePub = Types.Publication;
    }

 function getTypesPublicationAttack() external view returns (uint256) {
        return _referencePub;
    }

Tools Used

Mythx
Visual Studio Code
Foundry

Recommended Mitigation Steps

Initialize variable "_referencePub" or set the storage attribute "memory".

Fix

The solution or patch to the vulnerability is depicted as follow

        Types.Publication memory _referencePub;

Assessed type

Access Control

Don’t Use Openzeppelin’s Address.isContract() to Check Caller’s Address

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/3153fe2f6605a1f9b992c833107a5c21a887303d/contracts/base/LensProfiles.sol#L51
https://github.com/code-423n4/2023-07-lens/blob/3153fe2f6605a1f9b992c833107a5c21a887303d/contracts/base/LensBaseERC721.sol#L475
https://github.com/code-423n4/2023-07-lens/blob/3153fe2f6605a1f9b992c833107a5c21a887303d/contracts/namespaces/LensHandles.sol#L49

Vulnerability details

Impact

Address.isContract(address) is often used to determine whether a caller is an EOA( Externally Owned Address) or a contract, and it will return false for the following types of address:
1.an externally-owned account
2.a contract in construction
3.an address where a contract will be created
4.an address where a contract lived but was destroyed
The third and the fourth ones won’t cause any trouble using isContract since they are already destroyed or not created yet. However, the second one makes it possible to bypass isContract, like isContract(msg.sender)

Proof of Concept

https://github.com/code-423n4/2023-07-lens/blob/3153fe2f6605a1f9b992c833107a5c21a887303d/contracts/base/LensProfiles.sol#L51
https://github.com/code-423n4/2023-07-lens/blob/3153fe2f6605a1f9b992c833107a5c21a887303d/contracts/base/LensBaseERC721.sol#L475
https://github.com/code-423n4/2023-07-lens/blob/3153fe2f6605a1f9b992c833107a5c21a887303d/contracts/namespaces/LensHandles.sol#L49

modifier onlyEOA() {
        if (msg.sender.isContract()) {
            revert Errors.NotEOA();
        }
        _;
    }

a modifier like this can bypass many validation in different functions and cause trouble in the protocol

Tools Used

Manual Review

Recommended Mitigation Steps

it is not recommended to directly use the return value of Address.isContract() to determine whether a caller is a contract or not. require(msg.sender == tx.orign) works now and is a better practice.

Assessed type

Invalid Validation

Unrestricted Access to Follow Module Initialization

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/4ad521f84c43771d365ec0b35f904158201943fb/contracts/libraries/MigrationLib.sol#L141

Vulnerability details

Description

The Migration contract includes a function batchMigrateFollowModules which allows anyone to call it. Specifically, in this function, an arbitrary caller can set themselves as the transactionExecutor for the initializeFollowModule function of the new follow module.

Here is the concerned code snippet in the batchMigrateFollowModules function:

if (currentFollowModule == legacyFeeFollowModule) {
    StorageLib.getProfile(profileIds[i]).followModule = newFeeFollowModule;
    ILegacyFeeFollowModule.ProfileData memory feeFollowModuleData = ILegacyFeeFollowModule(
        legacyFeeFollowModule
    ).getProfileData(profileIds[i]);
    IFollowModule(newFeeFollowModule).initializeFollowModule({
        profileId: profileIds[i],
        transactionExecutor: msg.sender,
        data: abi.encode(
            feeFollowModuleData.currency,
            feeFollowModuleData.amount,
            feeFollowModuleData.recipient
        )
    });
}

The interface IFollowModule includes the initializeFollowModule function:

function initializeFollowModule(
    uint256 profileId,
    address transactionExecutor,
    bytes calldata data
) external returns (bytes memory);

Here, `transactionExecutor is expected to have the ability to transfer funds according to the annotations of the interface.

Impact

This vulnerability could potentially allow an unauthorized attacker to manipulate the initialization of follow modules in the Lens V2 system. If the initializeFollowModule function grants the transactionExecutor any form of control or influence over the module, this could be a critical vulnerability, allowing an attacker to gain control over follow modules by calling batchMigrateFollowModules with their own address as msg.sender.

Proof of Concept

Proof of Concept

  •   An attacker identifies a profile with a `legacyFeeFollowModule` or a `legacyProfileFollowModule`.
    
  •   The attacker calls `batchMigrateFollowModules` function with the identified profile's ID in the profileIds array, and the appropriate legacy module addresses in `legacyFeeFollowModule` or `legacyProfileFollowModule`. For `newFeeFollowModule`, they use the address of a malicious follow module they control.
    
  •   The `batchMigrateFollowModules` function sets the `followModule` of the identified profile to the new malicious follow module.
    
  •   The initializeFollowModule function of the malicious follow module is then called, with the attacker's address set as the `transactionExecutor`.
    

Recommended Mitigation Steps

To mitigate this potential issue, the following steps are recommended:

Add appropriate access controls to the batchMigrateFollowModules function, ensuring that only authorized addresses are allowed to call it.

In the initializeFollowModule function of the new module, implement proper access control mechanisms and do not allow arbitrary addresses to set critical parameters or perform critical actions. If such actions are necessary, they should only be executable by trusted addresses or the original owner of the profile.

Assessed type

Other

CollectNFT.sol can be minted by contracts that are unable to handle them

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/4ad521f84c43771d365ec0b35f904158201943fb/contracts/modules/act/collect/CollectNFT.sol#L54-L61

Vulnerability details

Impact

The CollectNFT.sol contract when minting uses _mint() instead of _safeMint() which can cause to mint a NFT to a contract who does not support nfts. So if user don't know well ERC721 can mint NFT to contract which not support ERC721 and stuck there forever.

Proof of Concept

    function mint(address to) external override onlyActionModule returns (uint256) {
        unchecked {
            uint256 tokenId = ++_tokenIdCounter;
            _mint(to, tokenId);
            return tokenId;
        }
    }

Tools Used

Manual Review

Recommended Mitigation Steps

The OZ _safeMint function should be used instead of _mint.

Assessed type

ERC721

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

setBlockStatus() and setBlockStatusWithSig() of LensHub.sol dont have return types defined but have return statements in logic.

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/LensHub.sol#L397-L403
https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/LensHub.sol#L406-L414

Vulnerability details

Impact

setBlockStatus() and setBlockStatusWithSig() dont have return types defined but have return statements in logic.

Proof of Concept

    /// @inheritdoc ILensProtocol
    function setBlockStatus(
        uint256 byProfileId,
        uint256[] calldata idsOfProfilesToSetBlockStatus,
        bool[] calldata blockStatus
    ) external override whenNotPaused onlyProfileOwnerOrDelegatedExecutor(msg.sender, byProfileId) {
        return ProfileLib.setBlockStatus(byProfileId, idsOfProfilesToSetBlockStatus, blockStatus);
    }

    /// @inheritdoc ILensProtocol
    function setBlockStatusWithSig(
        uint256 byProfileId,
        uint256[] calldata idsOfProfilesToSetBlockStatus,
        bool[] calldata blockStatus,
        Types.EIP712Signature calldata signature
    ) external override whenNotPaused onlyProfileOwnerOrDelegatedExecutor(signature.signer, byProfileId) {
        MetaTxLib.validateSetBlockStatusSignature(signature, byProfileId, idsOfProfilesToSetBlockStatus, blockStatus);
        return ProfileLib.setBlockStatus(byProfileId, idsOfProfilesToSetBlockStatus, blockStatus);
    }

Tools used

VS CODE
RECOMMENDED MITIGATION
Since ProfileLib.setBlockStatus() dont return anything, remove the return keyword in their logic.

Assessed type

Error

QA Report

See the markdown file with the details of this report here.

Anyone can initialize `LensHubInitializable` if `REVISION` is increased

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/misc/LensV2UpgradeContract.sol#L55
https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/misc/LensHubInitializable.sol#L54

Vulnerability details

Impact

Existing LensV2UpgradeContract only calls proxy_upgrade(without initialize it). Because LensHubInitializable is VersionedInitializable, if REVISION is changed, anyone can re-initialize the LensHubInitializable.

The REVISION of LensHub and LensHubInitializable are same(is ONE). Hence, it is not a hight-risk vulnerability now. However, it is highly advised to prevent this kind of risk.

Proof of Concept

No unit tests for LensV2UpgradeContract. I have added unit tests(and POC) for it.
LensV2UpgradeContract Revision vulnerability POC

AndyJiangPro is a private repository. Please let me know your github-handle and I will invite.

How to run the POC:

forge test -vvv --match-path ./test/misc/LensV2UpgradeContract.t.sol --match-test testLensV2UpgradeContract_NewRevision

output:

[PASS] testLensV2UpgradeContract_NewRevision() (gas: 149548)
Logs:
  0x3A383B39c10856a75B9E3f6eda6fCC8fC3334050
  gov before hack: 0x2946259E0334f33A064106302415aD3391BeD384
  gov after hack: 0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF

Tools Used

Foundry

Recommended Mitigation Steps

  1. Add authentication in LensHubInitializable::initialize
  2. Validate the old and new implementation REVISION, and initialize must be called if they are different.

Assessed type

Access Control

Blocked follower can keep follow with `batchMigrateFollows`

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/misc/LensV2Migration.sol#L37-L43
https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/libraries/MigrationLib.sol#L114-L139
https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/FollowNFT.sol#L480-L520

Vulnerability details

Impact

Blocked followers can follow by batchMigrateFollows.

Proof of Concept

You can migrate V1 followers by calling the LensV2Migration.batchMigrateFollows function, which can be called by anyone.

function batchMigrateFollows(
    uint256[] calldata followerProfileIds,
    uint256[] calldata idsOfProfileFollowed,
    uint256[] calldata followTokenIds
) external {
    MigrationLib.batchMigrateFollows(followerProfileIds, idsOfProfileFollowed, followTokenIds);
}

https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/misc/LensV2Migration.sol#L37-L43

function _migrateFollow(
    uint256 followerProfileId,
    uint256 idOfProfileFollowed,
    uint256 followTokenId
) private {
    uint48 mintTimestamp = FollowNFT(StorageLib.getProfile(idOfProfileFollowed).followNFT).tryMigrate({
        followerProfileId: followerProfileId,
        followerProfileOwner: StorageLib.getTokenData(followerProfileId).owner,
        idOfProfileFollowed: idOfProfileFollowed,
        followTokenId: followTokenId
    });
    // `mintTimestamp` will be 0 if:
    // - Follow NFT was already migrated
    // - Follow NFT does not exist or was burnt
    // - Follower profile Owner is different from Follow NFT Owner
    if (mintTimestamp != 0) {
        emit Events.Followed({
            followerProfileId: followerProfileId,
            idOfProfileFollowed: idOfProfileFollowed,
            followTokenIdAssigned: followTokenId,
            followModuleData: '',
            processFollowModuleReturnData: '',
            timestamp: mintTimestamp // The only case where this won't match block.timestamp is during the migration
        });
    }
}

https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/libraries/MigrationLib.sol#L114-L139

The FollowNFT.tryMigrate is where the actual migration logic proceed. FollowNFT.tryMigrate does not check whether the followerProfileId has been blocked by the idOfProfileFollowed.

function tryMigrate(
    uint256 followerProfileId,
    address followerProfileOwner,
    uint256 idOfProfileFollowed,
    uint256 followTokenId
) external onlyHub returns (uint48) {
    // Migrated FollowNFTs should have `originalFollowTimestamp` set
    if (_followDataByFollowTokenId[followTokenId].originalFollowTimestamp != 0) {
        return 0; // Already migrated
    }

    if (_followedProfileId != idOfProfileFollowed) {
        revert Errors.InvalidParameter();
    }

    if (!_exists(followTokenId)) {
        return 0; // Doesn't exist
    }

    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
    }

    unchecked {
        ++_followerCount;
    }

    _followTokenIdByFollowerProfileId[followerProfileId] = followTokenId;

    uint48 mintTimestamp = uint48(StorageLib.getTokenData(followTokenId).mintTimestamp);

    _followDataByFollowTokenId[followTokenId].followerProfileId = uint160(followerProfileId);
    _followDataByFollowTokenId[followTokenId].originalFollowTimestamp = mintTimestamp;
    _followDataByFollowTokenId[followTokenId].followTimestamp = mintTimestamp;

    super._burn(followTokenId);
    return mintTimestamp;
}

https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/FollowNFT.sol#L480-L520

Let's think the case that the idOfProfileFollowed profile blocked the followerProfileId when the follower has not yet migrated. In this case, if the owner of the followerProfileId or anyone else calls LensV2Migration.batchMigrateFollows, then the blocked followerProfileId can follow the idOfProfileFollowed.

The following codes are the PoC codes. Add and modify https://github.com/code-423n4/2023-07-lens/blob/main/test/migrations/Migrations.t.sol to run PoC.

First, modify the test because it is broken due to a change of the return value of getProfile. Add the following interface at Migrations.t.sol test file.

interface LensHubV1 {
    struct ProfileV1 {
        uint256 pubCount; // offset 0
        address followModule; // offset 1
        address followNFT; // offset 2
        string __DEPRECATED__handle; // offset 3
        string imageURI; // offset 4
        string __DEPRECATED__followNFTURI;
    }

    function getProfile(uint256 profileId) external view returns (ProfileV1 memory);
}

Also modify the following code to recover broken tests. https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/test/migrations/Migrations.t.sol#L107

address followNFTAddress = LensHubV1(address(hub)).getProfile(idOfProfileFollowed).followNFT;

Add this test function at MigrationsTest contract and run. Even after being blocked, it is possible to follow through batchMigrateFollows.

function testMigrateBlockedFollowerPoC() public onlyFork {
    uint256 idOfProfileFollowed = 8;

    uint256[] memory idsOfProfileFollowed = new uint256[](10);
    uint256[] memory followTokenIds = new uint256[](10);
    bool[] memory blockStatuses = new bool[](10);

    for (uint256 i = 0; i < 10; i++) {
        uint256 followTokenId = i + 1;

        idsOfProfileFollowed[i] = idOfProfileFollowed;
        followTokenIds[i] = followTokenId;

        blockStatuses[i] = true;
    }

    // block followers
    address targetProfileOwner = hub.ownerOf(idOfProfileFollowed);
    vm.prank(targetProfileOwner);

    hub.setBlockStatus(
        idOfProfileFollowed,
        followerProfileIds,
        blockStatuses
    );

    // check block status
    assertEq(hub.isBlocked(followerProfileIds[0], idOfProfileFollowed), true);

    // migrate
    hub.batchMigrateFollows(followerProfileIds, idsOfProfileFollowed, followTokenIds);

    // check follow work 
    address followNFTAddress = LensHubV1(address(hub)).getProfile(idOfProfileFollowed).followNFT;
    assertEq(FollowNFT(followNFTAddress).isFollowing(followerProfileIds[0]), true); // blocked, but followed!
}

Recommended Mitigation Steps

At FollowNFT.tryMigrate function, If the follower is blocked, make it unfollowed.

Assessed type

Invalid Validation

QA Report

See the markdown file with the details of this report here.

Contract DoS Attack

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/5917aab811803c29ed883f1d63002c202cf28f78/contracts/libraries/MigrationLib.sol#L142-L148

Vulnerability details

Impact

Contract DoS Attack

Proof of Concept

see this link code:
https://github.com/code-423n4/2023-07-lens/blob/5917aab811803c29ed883f1d63002c202cf28f78/contracts/libraries/MigrationLib.sol#L142-L148

This function is external, which mean everyone can call this function.
This function has a while loop, and the number of loops depends on the profileIds parameter.
A hacker can set up a large array to keep calling this function, resulting in de facto DOS attack.

Tools Used

nothing, just call function

Recommended Mitigation Steps

Set a maximum length check for the profileIds parameter, such as length must be less than 100

Assessed type

DoS

Contract DoS Attack

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/5917aab811803c29ed883f1d63002c202cf28f78/contracts/libraries/MigrationLib.sol#L94-L112

Vulnerability details

Impact

Contract DoS Attack

Proof of Concept

see this link:
https://github.com/code-423n4/2023-07-lens/blob/5917aab811803c29ed883f1d63002c202cf28f78/contracts/libraries/MigrationLib.sol#L94C1-L112C6
The function MigrationLib.batchMigrateFollows is external, called by external function LensV2Migration.batchMigrateFollows.
https://github.com/code-423n4/2023-07-lens/blob/5917aab811803c29ed883f1d63002c202cf28f78/contracts/misc/LensV2Migration.sol#L37C1-L43C6

This batchMigrateFollows function has a while loop, and the number of loops depends on the followerProfileIds[i], idsOfProfileFollowed[i], followTokenIds[i] parameters.

The two function not check parameters's length.
A hacker can set up a large array to keep calling this function, resulting in de facto DOS attack.

Tools Used

nothing, just call function

Recommended Mitigation Steps

Set a maximum length check for the followerProfileIds parameter, such as length must be less than 100

Assessed type

DoS

Analysis

See the markdown file with the details of this report here.

_abiEncode implementation error resulting in a signature conflict

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/libraries/MetaTxLib.sol#L190
https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/libraries/MetaTxLib.sol#L235-L237
https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/libraries/MetaTxLib.sol#L265-L267

Vulnerability details

Impact

The _abiEncode implementation error ignores the first bytes32 parameter of the struct. The difference between QuoteSignature and CommentSignature is the first Typehash parameter, so QuoteSignature and CommentSignature conflict. The user wants to sign the Quote but the signature can also be used for Comment.

Proof of Concept

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";

contract TestAbiEncode is Test {
    struct ReferenceParamsForAbiEncode {
        bytes32 typehash;
        uint256 a;
    }

    function _abiEncode(ReferenceParamsForAbiEncode memory referenceParamsForAbiEncode)
        private
        pure
        returns (bytes memory)
    {
        bytes memory encodedStruct = abi.encode(referenceParamsForAbiEncode);
        assembly {
            let lengthWithoutOffset := sub(mload(encodedStruct), 32) // Calculates length without offset.
            encodedStruct := add(encodedStruct, 32) // Skips the offset by shifting the memory pointer.
            mstore(encodedStruct, lengthWithoutOffset) // Stores new length, which now excludes the offset.
        }
        return encodedStruct;
    }

    function testReturn(bytes32 typehash, uint256 a) public {
        assertEq(_abiEncode(ReferenceParamsForAbiEncode({
            typehash: typehash,
            a: a
        })), abi.encodePacked(a));
    }
}

From the POC, you can see that the signature is independent of the first bytes32 parameter.

    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,
                actionModulesInitDataHash,
                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,
                actionModulesInitDataHash,
                quoteParams.referenceModule,
                referenceModuleInitDataHash,
                nonce,
                deadline
            )
        );
        _validateRecoveredAddress(_calculateDigest(keccak256(encodedAbi)), signature);
    }

As you can see from the code, the difference between QuoteSignature and CommentSignature is only the first Typehash parameter.
It is not clear why the design is so. Since comment and quote are two actions, the same signature should not be used. If someone makes a frontun tx to performs another action, the final result will not meet the user's expectation.

Tools Used

Foundry

Recommended Mitigation Steps

Use abi.encode

Assessed type

Context

The setBlockStatusWithSig function has acks proper access controla It allows anyone, even an attacker, to impersonate another profile owner and change the block status of profiles.

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/LensHub.sol#L406

Vulnerability details

Impact

 /// @inheritdoc ILensProtocol
    function setBlockStatusWithSig(
        uint256 byProfileId,
        uint256[] calldata idsOfProfilesToSetBlockStatus,
        bool[] calldata blockStatus,
        Types.EIP712Signature calldata signature
    ) external override whenNotPaused onlyProfileOwnerOrDelegatedExecutor(signature.signer, byProfileId) {
        MetaTxLib.validateSetBlockStatusSignature(signature, byProfileId, idsOfProfilesToSetBlockStatus, blockStatus);
        return ProfileLib.setBlockStatus(byProfileId, idsOfProfilesToSetBlockStatus, blockStatus);
    }

The setBlockStatusWithSig function has a problem that can allows anyone to change the block status of a profile by providing a valid signature from the victim's profile addres, it lacks proper access control checks, enabling attackers to impersonate the victim and block their profile.
an attacker can exploit this vulnerability and can cause Unauthorized Profile Blocking and Profile Manipulation and other malicious thing.

Proof of Concept

pOC to Exploit the Vulnerability:

  • so let's assume we have two profile IDs: attackerProfileId and victimProfileId.
  • The attacker will call the setBlockStatusWithSig function with the following parameters:
    - byProfileId : victimProfileId containe the victim's profile ID
    - idsOfProfilesToSetBlockStatus An array containing the victim's profile ID victimProfileId
    - blockStatus An array containing true, indicating that the victim's profile should be blocked.
    - signature The attacker will generate a forged signature that appears to be signed by the victim's address, granting permission to change the block status.

The malicious POC will look something like this:

// Assuming you have the attacker's private key for the `signature` creation
function exploitVulnerability(address lensHubContractAddress, uint256 victimProfileId, address attackerAddress) external {
    // Craft the data for the `setBlockStatusWithSig` function call
    bytes32 dataHash = keccak256(abi.encodePacked(victimProfileId, true));
    Types.EIP712Signature memory forgedSignature = MetaTxLib.createEIP712Signature(dataHash, attackerAddress);

    // Now call the vulnerable function with the forged signature
    LensHub lensHub = LensHub(lensHubContractAddress);
    lensHub.setBlockStatusWithSig(victimProfileId, [victimProfileId], [true], forgedSignature);
}

Tools Used

manual review

Recommended Mitigation Steps

  • the setBlockStatusWithSig function should enforce proper access control and signature verification.
  • Add proper access control checks in the setBlockStatusWithSig function to ensure only authorized actors can execute the function.
  • ensure that the MetaTxLib.validateSetBlockStatusSignature function properly verifies the signature and that it comes from the expected signer

Assessed type

Other

Missing whenNotPaused

Lines of code

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
https://github.com/code-423n4/2023-07-lens/blob/main/contracts/misc/LensV2Migration.sol#L45

Vulnerability details

Impact

In case where the governance wants to stop all activity, they still can't stop migrate.
And this problem just like this:
code-423n4/2022-02-aave-lens-findings#71

Proof of Concept

As we can see, The LenHub is inherits from LensV2Migration. And all the external function of LensHub have whenNotPasued modifier. However, the LensV2Migration 's function does not have the whenNotPasued modifier.

contract LensHub is
    LensProfiles,
    LensGovernable,
    LensV2Migration,
    LensImplGetters,
    LensHubEventHooks,
    LensHubStorage,
    ILensProtocol
{

Tools Used

vs code

Recommended Mitigation Steps

add whenNotPasued to all the external function of LensV2Migration.
And we can refer to this issue:
code-423n4/2022-02-aave-lens-findings#71

Assessed type

Error

user A(a spammer/fake news poster) can block user B and comment on user B posts but user B cannot reply the user A spam/fake news comment to debunk

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/LensHub.sol#L247
https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/libraries/PublicationLib.sol#L239
https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/libraries/ValidationLib.sol#L69

Vulnerability details

Impact

Note: this issue is not about reference modules, which will feature in my description but about how validateNotBlocked() is used in PublicationLib.sol and by extension LensHub.sol

In the comment() logic flow in LensHub.sol, the validateNotBlocked() function in validation library is called, it checks that the commenter (referencePubParams.profileId) is not blocked by the post author (referencePubParams.pointedProfileId). However, the commenter (referencePubParams.profileId) may have the post author (referencePubParams.pointedProfileId) blocked but commenter will still be able to comment since validateNotBlocked() only checks the mapping for blockedStatus(byProfile)[profile] without checking for blockedStatus(profile)[byProfile] too.

If user A blocks user B, user A should not be able to comment on user B posts. Blocking is done because a user doesnt want to interact/view a certain users' post. Interaction should be stopped if either user A or user B blocks the other. It should work both ways.

Bad actors can take advantage of this and spam the comment section/post fake news and the post author will not be able to comment/quote this activity to maybe issue a disclaimer or debunk it because post author is blocked by commenter. This is in my opinion unfair to the post author. Very similar scenario is explained here in this tweet --> https://twitter.com/dabit3/status/1682407045599989760?s=20

I noticed that for FollowerOnlyReferenceModule and DegreesOfSeparationReferenceModule, in this scenario explained above, comment will fail only because in its function processComment() it checks if the commenter is following the post author. When you block an account on lensV2, you stop following that account. And their processComment() reverts if commenter is not following the post author.

https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/modules/libraries/FollowValidationLib.sol#L21

        FollowValidationLib.validateIsFollowing({
            hub: HUB,
            followerProfileId: processCommentParams.profileId,
            followedProfileId: processCommentParams.pointedProfileId
        });
//this library call will revert the execution 

But the scenario is true for the other type of reference module like TokenGatedReferenceModule which checks only the token balance of the commenter.

Other other social networks like facebook and instagram dont allow u to comment/view posts of poeople you have blocked. It may be argued that the frontend wont show me posts of people i blocked in my feed, but it can be possible to comment via direct smart contract calls.

Proof of Concept

Note: this issue is about lensHub.sol comment() and the validateNotBlocked() in validation library not catching this described case.

Test File demonstrating this POC is found here --> https://gist.github.com/adeolu98/4b3b737c9f4ebbb8d7a9565338ef0318

LensHub.sol comment() fcn below

    /// @inheritdoc ILensProtocol
    function comment(Types.CommentParams calldata commentParams)
        external
        override
        whenPublishingEnabled
        onlyProfileOwnerOrDelegatedExecutor(msg.sender, commentParams.profileId)
        returns (uint256)
    {
        return PublicationLib.comment({commentParams: commentParams, transactionExecutor: msg.sender});
    }

comment() in PublicationLib.sol --> https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/libraries/PublicationLib.sol#L63-L94
validateNotblocked() is called in _createReferencePublication() which is called in comment() in PublicationLib.sol

https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/libraries/ValidationLib.sol#L69-L73

        ValidationLib.validateNotBlocked({
            profile: referencePubParams.profileId,
            byProfile: referencePubParams.pointedProfileId
        });
    function validateNotBlocked(uint256 profile, uint256 byProfile) internal view {
        if (StorageLib.blockedStatus(byProfile)[profile]) {
            revert Errors.Blocked();
        }
    }

validateNotBlocked() in validateLib checks the mapping for if blockedStatus[byProfile][profile] is true
but doesnt check for if blockedStatus[profile][byProfile] is also true.

processComment of TokenGatedReferenceModule only checks if commenter has the token balance,
https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/modules/reference/TokenGatedReferenceModule.sol#L84

    /**
     * @inheritdoc IReferenceModule
     * @dev Validates that the commenting profile's owner has enough balance of the gating token.
     *
     * @return balance The ABI-encoded gate token balance of the profile trying to comment/quote/mirror.
     */
    function processComment(
        Types.ProcessCommentParams calldata processCommentParams
    ) external view override onlyHub returns (bytes memory) {
        return
            abi.encode(
                _validateTokenBalance(
                    processCommentParams.profileId,
                    processCommentParams.pointedProfileId,
                    processCommentParams.pointedPubId
                )
            );
    }

    /**
     * @dev Validates the profile's owner balance of gating token. It can work with both ERC20 and ERC721 as both
     * interfaces shares `balanceOf` function prototype.
     *
     * @param profileId The ID of the profile trying to comment/quote/mirror.
     * @param pointedProfileId The ID of the pointed publication's author.
     * @param pointedPubId The ID of the pointed publication.
     *
     * @return uint256 The gate token balance of the profile trying to comment/quote/mirror.
     */
    function _validateTokenBalance(
        uint256 profileId,
        uint256 pointedProfileId,
        uint256 pointedPubId
    ) internal view returns (uint256) {
        GateParams memory gateParams = _gateParams[pointedProfileId][pointedPubId];
        uint256 balance = IToken(gateParams.tokenAddress).balanceOf(IERC721(HUB).ownerOf(profileId));
        if (balance < gateParams.minThreshold) {
            revert NotEnoughBalance();
        }
        return balance;
    }

it is good to check if any of the two participants of conversation have blocked the other and then prevent the conversation from happening based on that.

Tools Used

vs code

Recommended Mitigation Steps

add a check for if commenter has blocked the post author. So if there is a block by the post author, commenter won't comment and if there is a block by commenter, commenter wont be able to comment on posts from an author thats it has blocked.

    function validateNotBlocked(uint256 profile, uint256 byProfile) internal view {
        if (StorageLib.blockedStatus(byProfile)[profile] || StorageLib.blockedStatus(profile)[byProfile]) {
            revert Errors.Blocked();
        }
    }

Assessed type

Invalid Validation

QA Report

See the markdown file with the details of this report here.

Contract DoS Attack: Post function

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/main/contracts/libraries/PublicationLib.sol#L472-L491
https://github.com/code-423n4/2023-07-lens/blob/5917aab811803c29ed883f1d63002c202cf28f78/contracts/libraries/PublicationLib.sol#L22-L36
https://github.com/code-423n4/2023-07-lens/blob/5917aab811803c29ed883f1d63002c202cf28f78/contracts/LensHub.sol#L224-L232

Vulnerability details

Impact

DoS Attack
When a hacker Profile, he can spam posts indefinitely, resulting in de facto dos of the contract。

Proof of Concept

see this 3 link code:
https://github.com/code-423n4/2023-07-lens/blob/5917aab811803c29ed883f1d63002c202cf28f78/contracts/LensHub.sol#L224-L232
https://github.com/code-423n4/2023-07-lens/blob/5917aab811803c29ed883f1d63002c202cf28f78/contracts/libraries/PublicationLib.sol#L22-L36
https://github.com/code-423n4/2023-07-lens/blob/main/contracts/libraries/PublicationLib.sol#L472-L491

Function call chain:
-> LensHub.post(Types.PostParams calldata postParams) external
-> PublicationLib.post(Types.PostParams calldata postParams, address transactionExecutor) external
-> PublicationLib._initPubActionModules(...,postParams.actionModules,postParams.actionModulesInitDatas)

Attack method:
This _initPubActionModules function has a while loop, and the number of loops depends on the actionModules, actionModulesInitDatas parameters.
the postParams.actionModules,postParams.actionModulesInitDatas parameters controled by user's call data postParams.
So the attack can set up a long array and keep calling the LensHub.post function. The while loop will execute heavily, resulting in de facto dos.

Tools Used

nothing, just call post function

Recommended Mitigation Steps

As a rule, do not leave the number of for while loops to the external user.
Just like Twitter, set a maximum word count. must set a maximum calldata length.

Assessed type

DoS

QA Report

See the markdown file with the details of this report here.

Analysis

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

Bypassing onlyEOA Modifier and Unprotected Access to getTokenGuardianDisablingTimestamp

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/namespaces/LensHandles.sol#L187

Vulnerability details

Impact

Severity: Medium
Likelihood: Medium

Proof of Concept

The DANGER__disableTokenGuardian function in the LensHandles.sol contract uses the onlyEOA modifier to restrict access to external owned accounts (EOAs). However, this restriction can be bypassed by calling the function from the constructor of another contract. This could allow an attacker to disable the token guardian, potentially leading to unauthorized actions.

Additionally, the getTokenGuardianDisablingTimestamp function does not use the onlyEOA modifier, which means that _tokenGuardianDisablingTimestamp[wallet] can be created from a contract. This could lead to unexpected behavior or potential manipulation of the disabling timestamp.

An attacker could create a new contract that calls the DANGER__disableTokenGuardian function in its constructor. Since the constructor is called during contract creation, it is considered an EOA during that transaction, allowing it to bypass the onlyEOA modifier.

contract Attacker {
    constructor(LensHandles target) {
        target.DANGER__disableTokenGuardian();
    }
}

Tools Used

Manual Code Review

Recommended Mitigation Steps

Apply the onlyEOA modifier or similar access controls to the getTokenGuardianDisablingTimestamp function to prevent it from being called from a contract. This could help prevent potential misuse or manipulation of the disabling timestamp.

Assessed type

Access Control

THe follow function have a Length Mismatch can lead to out-of-bounds access or unexpected behavior

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/libraries/FollowLib.sol#L15

Vulnerability details

Impact

the follow function has a lack of proper validation to ensure that the arrays idsOfProfilesToFollow, and followTokenIds, and followModuleDatas have the same length. so this means that an attacker can pass different length arrays, causing an array length mismatch. As a result, the loop iterating over these arrays might access out-of-bounds elements, leading to unexpected behavior or even crashes of the contract

Proof of Concept

  • the attacker's can create a evil contract and tries to trigger the follow function of the vulnerable contract FollowLib. It provides mismatched array lengths for idsOfProfilesToFollow, followTokenIds, and followModuleDatas, so the follow function will iterate through these arrays without proper bounds checking, potentially leading to unexpected behavior or even causing the contract to revert.

Tools Used

manual review

Recommended Mitigation Steps

ensure that all three arrays (idsOfProfilesToFollow, followTokenIds, and followModuleDatas) have the same length before proceeding with the loop.

Assessed type

Other

`validateBurnSignature` does not check signers ownership of `tokenID`

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/main/contracts/libraries/MetaTxLib.sol#L309

Vulnerability details

Impact

if validateBurnSignature is used as a source of truth it could validate falsely, since it does not validate that the signer is the owner of the tokenID.
Anyone can sign a message with any tokenID and pass the validation.
https://github.com/code-423n4/2023-07-lens/blob/main/contracts/libraries/MetaTxLib.sol#L309

function validateBurnSignature(Types.EIP712Signature calldata signature, uint256 tokenId) external {
        _validateRecoveredAddress(
            _calculateDigest(
                keccak256(
                    abi.encode(Typehash.BURN, tokenId, _getAndIncrementNonce(signature.signer), signature.deadline)
                )
            ),
            signature
        );
    }

Proof of Concept

https://github.com/code-423n4/2023-07-lens/blob/main/contracts/libraries/MetaTxLib.sol#L309

Tools Used

Manual Review

Recommended Mitigation Steps

check if the owner of the tokenID is the signer of the signature.

Assessed type

Invalid Validation

MigrationLib - Profile migration does not include V1's dispatcher migration

Lines of code

https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/libraries/MigrationLib.sol#L60-L90

Vulnerability details

Impact

In Lens V2, the idea of delegated executers is introduced, of where each delegated executor plays the dispatcher role in V1.
However, in profile migration, dispatchers are not migrated, which means that in V2, V1's dispatchers will not be able to interact with LensHub as users.

Proof of Concept

Modifier for dispatcher in V1

https://github.com/lens-protocol/core/blob/2c843f827d9614bf633e989facd877301d97ce8b/contracts/core/LensHub.sol#L1022-L1027

How dispatcher is used in actions like post

https://github.com/lens-protocol/core/blob/2c843f827d9614bf633e989facd877301d97ce8b/contracts/core/LensHub.sol#L354-L370

Tools Used

Manual review

Recommended Mitigation Steps

When migrating a profile, add V1's dispatcher into delegated executors config.

Assessed type

DoS

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.