Aave Lens contest - pauliax's results

Web3 permissionless, composable & decentralized social graph

General Information

Platform: Code4rena

Start Date: 10/02/2022

Pot Size: $100,000 USDC

Total HM: 13

Participants: 21

Period: 7 days

Judge: leastwood

Total Solo HM: 10

Id: 85

League: ETH

Aave Lens

Findings Distribution

Researcher Performance

Rank: 10/21

Findings: 2

Award: $1,246.74

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: WatchPug

Also found by: 0x0x0x, 0x1f8b, 0xwags, Dravee, cccz, csanuragjain, defsec, gzeon, hubble, hyh, kenta, pauliax, sikorico

Labels

bug
QA (Quality Assurance)

Awards

1061.0996 USDC - $1,061.10

External Links

  • The LensNFTBase inherits a functionality of the Context contract of OpenZeppelin, which is designed to be used with Ethereum Gas Station Network (GSN):
contract LensNFTBase is ERC721Enumerable
contract ERC721Enumerable is ERC721Time
contract ERC721Time is Context

Context wants _msgSender() as a replacement for msg.sender. For regular transactions it returns msg.sender and for meta transactions it can be used to return the end user rather than the relayer. So I think LensNFTBase and all the contracts that inherit from it should use _msgSender(), not msg.sender, e.g.:

LensNFTBase

  function burn(uint256 tokenId) public virtual override {
      if (!_isApprovedOrOwner(msg.sender, tokenId)) revert Errors.NotOwnerOrApproved();
      ...

CollectNFT

    function mint(address to) external override {
        if (msg.sender != HUB) revert Errors.NotHub();

FollowNFT

    function mint(address to) external override {
        if (msg.sender != HUB) revert Errors.NotHub();
        uint256 tokenId = ++_tokenIdCounter;
        _mint(to, tokenId);
    }
    function delegate(address delegatee) external override {
        _delegate(msg.sender, delegatee);
    }

And there are many usages of msg.sender in LensHub. If you really need Context there, consider replacing msg.sender with _msgSender().

  • You could put this to Constants instead of duplicating it in multiple contracts:

FeeModuleBase

    uint16 internal constant BPS_MAX = 10000;

ModuleGlobals

   uint16 internal constant BPS_MAX = 10000;
  • Either the comment is a bit misleading or the code is not entirely correct:
@param PublishingPaused The state where only publication creation functions are paused.

However, whenPublishingEnabled modifier is applied not only to post functions but also comment and mirror. But maybe it's just me misinterpreting the situation and comment/mirror are also considered as publication creation functions.

  • Consider using 2-step governance change procedure to reduce a risk of accidentally setting a wrong address:
  function _setGovernance(address newGovernance) internal {
      if (newGovernance == address(0)) revert Errors.InitParamsInvalid();
      address prevGovernance = _governance;
      _governance = newGovernance;
      emit Events.ModuleGlobalsGovernanceSet(prevGovernance, newGovernance, block.timestamp);
  }
  function _setGovernance(address newGovernance) internal {
      address prevGovernance = _governance;
      _governance = newGovernance;
      emit Events.GovernanceSet(msg.sender, prevGovernance, newGovernance, block.timestamp);
  }
  • It is not reliable to use bytes(str).length to check the string length because encoded strings can differ in length, e.g., a single character encoded in UTF-8 can be more than a byte long.
  function _validateHandle(string calldata handle) private pure {
      bytes memory byteHandle = bytes(handle);
      if (byteHandle.length == 0 || byteHandle.length > Constants.MAX_HANDLE_LENGTH

More details: https://github.com/miguelmota/solidity-idiosyncrasies However, I do not see any serious issue with your implementation, because you allow only a whitelisted set of characters. so just added this FYI.

  • Name _dataByPublicationByProfile is a bit misleading, as it is actually more like _dataByProfileByPublication:
  _dataByPublicationByProfile[profileId][pubId]

but again, maybe you are reading it differently.

  • contract LensHubStorage should be abstract cuz it doesn't make sense on its own.

  • Fee modules have this comment, but there is no such thing as FeeCollectModuleBase:

 * @notice This is a simple Lens CollectModule implementation, inheriting from the ICollectModule interface and
 * the FeeCollectModuleBase abstract contract.

I think it should be FeeModuleBase.

  • Handle registrations can be frontrunned, but I am not sure if that's a big issue because onlyWhitelistedProfileCreator can do that, and the prevention would introduce unnecessary complexity.

  • The same sigNonces mapping is used for all the actions. To prevent replay attacks, upon successful execution sigNonces is incremented. This means that to sign unrelated actions a signer needs to wait for one by one to execute it on-chain, it can't happen in parallel. As an active user I want to sign txs to create a post, follow, delegate, change dispatcher, etc independently without waiting for one to finish before signing another.

  • Consider explicitly marking all the main user functions as non-reentrant, especially those that interact with modules, because many more modules can be whitelisted in the future and there is no guarantee that they will follow the Checks-Effects-Interactions pattern.

#0 - Zer0dot

2022-03-18T19:48:34Z

These are mostly valid, and helpful. I think a lot of this is opinion, but the sigNonces point is interesting and quite relevant. It's kept this way for simplicity but basically for streamlined actions we have the dispatcher system. Still, this is a high quality report!

#1 - 0xleastwood

2022-05-05T22:29:11Z

Great findings! The warden has shown the issues clearly and all seems to valid from the sponsor's POV.

Findings Information

🌟 Selected for report: Dravee

Also found by: 0x0x0x, 0x1f8b, IllIllI, Jujic, csanuragjain, d4rk, defsec, gzeon, nahnah, pauliax, rfa

Labels

bug
G (Gas Optimization)

Awards

185.645 USDC - $185.64

External Links

  • function _validatePublishingEnabled() would be cheapier if it did not check all the other states, but just Unpaused:
  if (_state != DataTypes.ProtocolState.Unpaused) {
    revert Errors...();
  }
  • Because MAX_HANDLE_LENGTH is 31, I think you can use bytes32 to store the handles. This would be cheaper than dynamic size strings or bytes.

  • The same storage is accessed many times in a function, e.g. _dataByPublicationByProfile[profileId][pubId] is accessed 3 times in _processCollect:

  uint256 amount = _dataByPublicationByProfile[profileId][pubId].amount;
  address currency = _dataByPublicationByProfile[profileId][pubId].currency;
  ...
  address recipient = _dataByPublicationByProfile[profileId][pubId].recipient;

Would be better to cache this in a variable and re-use.

  • I think it would be cheaper here to first increment the pubCount and the pass the updated value (avoid repeated add operation):
  PublishingLogic.createComment(
      vars,
      _profileById[vars.profileId].pubCount + 1,
      _profileById,
      _pubByIdByProfile,
      _collectModuleWhitelisted,
      _referenceModuleWhitelisted
  );
  _profileById[vars.profileId].pubCount++;
  • I wonder do you really need to pass the whole mapping to another function if you use just one element from it, e.g. function createComment in PublishingLogic library takes the whole _profileById mapping but uses only pubCount of one element:
   mapping(uint256 => DataTypes.ProfileStruct) storage _profileById
   ...
  // Validate existence of the pointed publication
  uint256 pubCount = _profileById[vars.profileIdPointed].pubCount;
  if (pubCount < vars.pubIdPointed || vars.pubIdPointed == 0)
    revert Errors.PublicationDoesNotExist();

Can't you just pass only data that you actually need and use, or even pass this pubCount of profileIdPointed from the caller? There are many more functions that take these big storage pointers as parameters, so in my opinion, you should consider optimizing this.

  • ++ and -- are a bit cheaper than += 1 and -= 1 :
_balances[to] += 1;
_balances[owner] -= 1;
_balances[from] -= 1;

#0 - Zer0dot

2022-03-18T17:49:44Z

The unpaused check is a good catch!

The 32 byte handles is something we've discussed but breaks the assumption that, via upgradeability, handles could be longer. Anyway right now they are stored in 32 byte words because length is packed in the same slot when it's < 32 bytes. The point about storage accesses is not valid because each element in a struct is accessed separately, loading the entire struct is less efficient. Incrementing the publication count with _createComment introduces a vulnerability that allows the comment to comment on itself, so that's invalid too. Passing the pointed pubCount directly increases the LensHub contract size and is not a tradeoff we want to take. Lastly, the increment point is made invalid by the optimizer.

#1 - Zer0dot

2022-03-18T18:01:33Z

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter