Skip to content
This repository has been archived by the owner on Jun 2, 2024. It is now read-only.

Commit

Permalink
Uploaded files for judging
Browse files Browse the repository at this point in the history
  • Loading branch information
sherlock-admin2 committed Dec 1, 2023
1 parent c679c9c commit c69a147
Show file tree
Hide file tree
Showing 327 changed files with 25,487 additions and 10 deletions.
10 changes: 0 additions & 10 deletions .gitignore

This file was deleted.

78 changes: 78 additions & 0 deletions 001/003.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
Uneven Clay Rook

medium

# Token.updateFounders will not clear tokenRecipient correctly

## Summary
Token.updateFounders function doesn't initialize `baseTokenId` as `reservedUntilTokenId` during position clearing for the owner. Because of incorrect offset it will be impossible to clear positions correctly, which leads to bigger amount of tokens for same owner.
## Vulnerability Detail
When new Token is created, then `_addFounders` [is called](https://github.com/sherlock-audit/2023-09-nounsbuilder/blob/main/nouns-protocol/src/token/Token.sol#L92).

This function loops through the founders and fetches their percentage of minted tokens during vesting. Then function populates `tokenRecipient` mapping to mark token ids that should belong to owner(tokenId % 100).
https://github.com/sherlock-audit/2023-09-nounsbuilder/blob/main/nouns-protocol/src/token/Token.sol#L164-L175
```solidity
uint256 baseTokenId = reservedUntilTokenId;
for (uint256 j; j < founderPct; ++j) {
// Get the available token id
baseTokenId = _getNextTokenId(baseTokenId);
// Store the founder as the recipient
tokenRecipient[baseTokenId] = newFounder;
emit MintScheduled(baseTokenId, founderId, newFounder);
// Update the base token id
baseTokenId = (baseTokenId + schedule) % 100;
}
```
In case if owner has 20% ownership that means that he will receive 20 out of 100 minted tokens during the vesting.
One thing that we should note here is that `baseTokenId` is initialized with `reservedUntilTokenId` which is the amount of tokens that are reserved. As result `tokenRecipient` values will be shifted by `reservedUntilTokenId`.

Using `updateFounders` function owner have ability to update founders.
First thing function is trying to clear `tokenRecipient` mapping, so all old owner's vesting positions are cleared.

https://github.com/sherlock-audit/2023-09-nounsbuilder/blob/main/nouns-protocol/src/token/Token.sol#L412-L427
```solidity
uint256 baseTokenId;
for (uint256 j; j < cachedFounder.ownershipPct; ++j) {
// Get the next index that hasn't already been cleared
while (clearedTokenIds[baseTokenId] != false) {
baseTokenId = (++baseTokenId) % 100;
}
delete tokenRecipient[baseTokenId];
clearedTokenIds[baseTokenId] = true;
emit MintUnscheduled(baseTokenId, i, cachedFounder);
// Update the base token id
baseTokenId = (baseTokenId + schedule) % 100;
}
```
This function is trying to clear all positions and mark them as cleared.
This code is really similar to the one that is used in `_addFounders` function, however `baseTokenId` is 0 here and is not initialized with `reservedUntilTokenId`.

Because `baseTokenId` is not initialized with `reservedUntilTokenId`, it's likely that all positions in the `tokenRecipient` will not be cleared as wrong token position will be assumed to belong to owner.

Example(the most simple):
- reservedUntilTokenId = 10, and we have 1 owner with 1% ownership, so `tokenRecipient[10] = owner1`
- now `updateFounders` is called and because `baseTokenId` is initialized as 0, then `tokenRecipient[10]` was not cleared
- same owner was set with percentage 1%(same percentage, so it's easier to explain), then `tokenRecipient[11] = owner1` and `tokenRecipient[10] = owner1` still remains

Now `owner1` has 2 tokens out of 100 instead of 1.
The problem is that in case if user has 1% then he receives only 1 additional token, but in case if he had bigger percentage, then he will get bigger amount of additional tokens.

This bug can be triggered in case if dao uses `reservedUntilTokenId` or have migrated from l1 to l2(as it's likely that `reservedUntilTokenId` will not be 0 only after migration) and after that `updateFounders` was called.
## Impact
Owner receives additional vesting tokens.
## Code Snippet
Provided above
## Tool used

Manual Review

## Recommendation
Initialize `baseTokenId` with `reservedUntilTokenId` value.
181 changes: 181 additions & 0 deletions 001/005.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
Cold Blonde Sparrow

high

# The `Token::updateFounders()` func does not remove the previous founders, which leads to them being able to claim tokens from the DAO

## Summary

The `Token::updateFounders()` function does not remove the previous founders, which leads to them being able to claim tokens.
## Vulnerability Detail

Founders are added to the DAO's founders list during the deployment of the DAO by using the [Token::_addFounders()](https://github.com/sherlock-audit/2023-09-nounsbuilder/blob/main/nouns-protocol/src/token/Token.sol#L92) function. As a result, the founder can obtain a token from the DAO. The `Token::_addFounders()` func starts from the [reservedUntilTokenId](https://github.com/sherlock-audit/2023-09-nounsbuilder/blob/main/nouns-protocol/src/token/Token.sol#L161) number in order to assign it to the corresponding `tokenRecipient` array in the [code line 169](https://github.com/sherlock-audit/2023-09-nounsbuilder/blob/main/nouns-protocol/src/token/Token.sol#L169):

```solidity
File: Token.sol
160: // Used to store the base token id the founder will recieve
161: uint256 baseTokenId = reservedUntilTokenId;
162:
163: // For each token to vest:
164: for (uint256 j; j < founderPct; ++j) {
165: // Get the available token id
166: baseTokenId = _getNextTokenId(baseTokenId);
167:
168: // Store the founder as the recipient
169: tokenRecipient[baseTokenId] = newFounder;
170:
171: emit MintScheduled(baseTokenId, founderId, newFounder);
172:
173: // Update the base token id
174: baseTokenId = (baseTokenId + schedule) % 100;
175: }
176: }
```

In the other hand, the owner can update the founders list using the [Token::updateFounders()](https://github.com/sherlock-audit/2023-09-nounsbuilder/blob/main/nouns-protocol/src/token/Token.sol#L375) func. The function will remove the previous assigned founders in the [code line 420](https://github.com/sherlock-audit/2023-09-nounsbuilder/blob/main/nouns-protocol/src/token/Token.sol#L420):

```solidity
File: Token.sol
411: // Used to reverse engineer the indices the founder has reserved tokens in.
412: uint256 baseTokenId;
413:
414: for (uint256 j; j < cachedFounder.ownershipPct; ++j) {
415: // Get the next index that hasn't already been cleared
416: while (clearedTokenIds[baseTokenId] != false) {
417: baseTokenId = (++baseTokenId) % 100;
418: }
419:
420: delete tokenRecipient[baseTokenId];
421: clearedTokenIds[baseTokenId] = true;
422:
423: emit MintUnscheduled(baseTokenId, i, cachedFounder);
424:
425: // Update the base token id
426: baseTokenId = (baseTokenId + schedule) % 100;
427: }
428: }
```

A problem arises because the function starts from the [baseTokenId zero](https://github.com/sherlock-audit/2023-09-nounsbuilder/blob/main/nouns-protocol/src/token/Token.sol#L412), an action that is incorrect because it does not consider the `reservedUntilTokenId` in the [code line 412](https://github.com/sherlock-audit/2023-09-nounsbuilder/blob/main/nouns-protocol/src/token/Token.sol#L412). As a result, previous founders remain on the list, enabling them to claim a token from the DAO.

## Impact

The [Token::updateFounders()](https://github.com/sherlock-audit/2023-09-nounsbuilder/blob/main/nouns-protocol/src/token/Token.sol#L375C14-L375C28) does not work as intended, it attempts to remove the previous founders from the `tokenRecipient[]` array but fails, leaving previous founders able to claim a token from the DAO.

I created a test where the DAO is deployed using a list of two founders and a `reservedUntilTokenId=10`, then the owner updates the founders using another list of founders but the previous founders still are in the `tokenRecipient[]` array.

```solidity
// File: Token.t.sol
// $ forge test --match-test "test_UpdateFundersDoesNotUseTheReservedUntilTokenId" -vvv
//
function test_UpdateFundersDoesNotUseTheReservedUntilTokenId() public {
//
// The previous founders are not removed once the owner calls updateFounders() using another founders list causing that
// the previous founders are able to claim a token
createUsers(2, 1 ether);
address[] memory wallets = new address[](2);
uint256[] memory percents = new uint256[](2);
uint256[] memory vestExpirys = new uint256[](2);
unchecked {
for (uint256 i; i < 2; ++i) {
wallets[i] = otherUsers[i];
percents[i] = 1;
vestExpirys[i] = 4 weeks;
}
}
deployWithCustomFoundersAndCustomReserve(wallets, percents, vestExpirys, 10);
//
// assert totalFounders is 1
assertEq(token.totalFounders(), 2);
assertEq(token.totalFounderOwnership(), 2);
//
// The scheduledRecipient starts in number `10` because the token was deployed using reservedUntilTokenId = 10
assertEq(token.getScheduledRecipient(10).wallet, otherUsers[0]);
assertEq(token.getScheduledRecipient(11).wallet, otherUsers[1]);
//
// The founders are updated but the previous founders are not removed
IManager.FounderParams[] memory newFoundersArr = new IManager.FounderParams[](1);
newFoundersArr[0] = IManager.FounderParams({
wallet: address(0x06B59d0b6AdCc6A5Dc63553782750dc0b41266a3),
ownershipPct: 10,
vestExpiry: 2556057600
});
vm.prank(address(wallets[0]));
token.updateFounders(newFoundersArr);
//
// Assert the previous founder still has the baseTokenId 10 and 11 which is incorrect
// because the previous founders list should had been removed
assertEq(token.getScheduledRecipient(10).wallet, otherUsers[0]);
assertEq(token.getScheduledRecipient(11).wallet, otherUsers[1]);
assertEq(token.getScheduledRecipient(12).wallet, address(0x06B59d0b6AdCc6A5Dc63553782750dc0b41266a3));
}
```

Additionally, the next funcion is added to the `NounsBuilderTest.sol` file:

```diff
+++ b/nouns-protocol/test/utils/NounsBuilderTest.sol
@@ -274,6 +274,25 @@ contract NounsBuilderTest is Test {
setMockMetadata();
}

+ function deployWithCustomFoundersAndCustomReserve(
+ address[] memory _wallets,
+ uint256[] memory _percents,
+ uint256[] memory _vestExpirys,
+ uint256 _reservedUntilTokenId
+ ) internal virtual {
+ setFounderParams(_wallets, _percents, _vestExpirys);
+
+ setMockTokenParamsWithReserve(_reservedUntilTokenId);
+
+ setMockAuctionParams();
+
+ setMockGovParams();
+
+ deploy(foundersArr, tokenParams, auctionParams, govParams);
+
+ setMockMetadata();
+ }
+
```

## Code Snippet

- [Token::_addFounders()](https://github.com/sherlock-audit/2023-09-nounsbuilder/blob/main/nouns-protocol/src/token/Token.sol#L92)
- [Token::updateFounders()](https://github.com/sherlock-audit/2023-09-nounsbuilder/blob/main/nouns-protocol/src/token/Token.sol#L375)


## Tool used

Manual review

## Recommendation

Please, ensure the usage of `reservedUntilTokenId` in the [Token::updateFounders()](https://github.com/sherlock-audit/2023-09-nounsbuilder/blob/main/nouns-protocol/src/token/Token.sol#L412) function:

```diff
File: Token.sol
410:
411: // Used to reverse engineer the indices the founder has reserved tokens in.
-- uint256 baseTokenId;
++ uint256 baseTokenId = reservedUntilTokenId;
413:
414: for (uint256 j; j < cachedFounder.ownershipPct; ++j) {
415: // Get the next index that hasn't already been cleared
416: while (clearedTokenIds[baseTokenId] != false) {
417: baseTokenId = (++baseTokenId) % 100;
418: }
419:
420: delete tokenRecipient[baseTokenId];
421: clearedTokenIds[baseTokenId] = true;
422:
423: emit MintUnscheduled(baseTokenId, i, cachedFounder);
424:
425: // Update the base token id
426: baseTokenId = (baseTokenId + schedule) % 100;
427: }
428: }
```

Moreover, adapt the necessary procedures if the `reservedUntilTokenId` changes via the [Token::setReservedUntilTokenId()](https://github.com/sherlock-audit/2023-09-nounsbuilder/blob/main/nouns-protocol/src/token/Token.sol#L486C14-L486C37) function.
50 changes: 50 additions & 0 deletions 001/008.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
Uneven Clay Rook

medium

# reservedUntilTokenId should not be used to calculate vested positions

## Summary
Because `reservedUntilTokenId` is used to calculated vested positions for owners it's likely that founders will get bigger amount of tokens than they should after migration.
## Vulnerability Detail
`reservedUntilTokenId` is parameter that marks amount of tokens, that should not be used using auction. They should be minted [only by special minter](https://github.com/sherlock-audit/2023-09-nounsbuilder/blob/main/nouns-protocol/src/token/Token.sol#L211-L217). The reason we need this param is to make migration to l2. When migration will start, then `reservedUntilTokenId` will be set to the amount of already minted tokens. So later minter can mint them all to the recipients on l2. So when dao is deployed on l1 it will have `reservedUntilTokenId` as 0.

Now we need to talk about how vesting positions are calculated for the owners.
https://github.com/sherlock-audit/2023-09-nounsbuilder/blob/main/nouns-protocol/src/token/Token.sol#L161-L176
```solidity
uint256 baseTokenId = reservedUntilTokenId;
// For each token to vest:
for (uint256 j; j < founderPct; ++j) {
// Get the available token id
baseTokenId = _getNextTokenId(baseTokenId);
// Store the founder as the recipient
tokenRecipient[baseTokenId] = newFounder;
emit MintScheduled(baseTokenId, founderId, newFounder);
// Update the base token id
baseTokenId = (baseTokenId + schedule) % 100;
}
}
```
As you can see, `baseTokenId` is initialized with `reservedUntilTokenId`. On l1 it will be 0 likely. Then we go through the percentage of founder and mark his vesting positions.

For example we have 2 founders with 51% and 5%.
- Then founder1 will have `tokenRecipient[0 - 50]` as his 51 vesting positions and founder2 will have `tokenRecipient[51;71;91;52;72]` as vesting positions.
- Then we minted 8 tokens through the action, which means that tokens[0-59] were minted and `founder1` got 51 token and `founder2` got 1 vesting token.
- Then dao decided to move to l2 and `reservedUntilTokenId` was set to 60 as such amount of tokens were minted already.
- Then on l2 during initialization vesting positions are recalculated using `reservedUntilTokenId`, so founder1 will have `tokenRecipient[0-10;60 - 99]` as his 51 vesting positions and founder2 will have `tokenRecipient[11;31;51;12;32]` as vesting positions.
- So additional 8 tokens were minted using auction, which means that tokens[60-119] were minted and `founder1` again got 51 token and `founder2` got 1 vesting token.
- As result we see that during that period founder1 got 102 vesting tokens, while founder2 got 2. And also we see that total minted amount is 120 and 104 of them is for founders, which is bigger than it should be.
## Impact
Incorrect vesting amount can occur after migration
## Code Snippet
Provided above
## Tool used

Manual Review

## Recommendation
I believe that you don't need to use `reservedUntilTokenId` to calculate vesting positions. Just start with 0 and it will work in all cases. Using `reservedUntilTokenId` you are protected that those amount ids will not be reused anymore and that's enough.
Loading

0 comments on commit c69a147

Please sign in to comment.