You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository has been archived by the owner on Jun 2, 2024. It is now read-only.
dimulski - Reserving 100 or more tokens for claiming when a DAO is created results in the first founder not receiving % of the NFTs entitled to him.
#233
DuplicateA valid issue that is a duplicate of an issue with `Has Duplicates` labelHighA valid High severity issueRewardA payout will be made for this issue
Reserving 100 or more tokens for claiming when a DAO is created results in the first founder not receiving % of the NFTs entitled to him.
Summary
When a DAO is created there are n number of tokens that can be reserved for a private sale, if the founders have decided to bootstrap the project in such a way, or mint NFTs to certain people on discount. When setting up the parameters for the DAO there is the option for different founders to be set whom in turn will receive certain percentage of all the minted NFTs, until their vesting period is over. NFTs grant voting power. For example Nouns DAO mints 1 of each 10 NFTs to the treasury, a single auction continues for a day. In another scenario auctions can also be held every 15 mins for example Lil Nouns. The problem arises if reservedUntilTokenId in Token.sol is set to 100 or more. NFTs are distributed to founders based on their ownership percentage. For example if the total ownership of all the founders is 10%, and regular users mint 100 NFTs, the total minted NFTs will be 110 and 10 NFTs will be distributed to the founders based on their percentage share. Keep in mind that creators of DAOs on the nounsbuilder protocol may choose to reserve a lot of tokens based on factors such as the duration of each auction, for example as mentioned above if auctions are held every 15 mins we can assume that the reserve tokens will be way above 100.
Note: This issue doesn't describe a malicious owner imputing parameters that will benefit him, or supplying a specific wrong parameter that triggers an edge case which in turns leads to a loss of value for the protocol or founders. This issue describes a scenario where an admin or governance enters a parameter that would be expected under normal conditions, and something is broken.
To better illustrate what is happening in the _addFounders() function add the following to the Token.sol contract:
import { console2 } from"forge-std/Test.sol";
At line 170 in the _addFounders() function add console2.log("From Token.sol _addFounders: ", baseTokenId);
Add the following test to Token.t.sol
function test_FounderLosesNFTShare() public {
deployMock();
// Mint 500 tokensfor (uint256 i =0; i <500; i++) {
vm.prank(address(auction));
token.mint();
}
console2.log("Number of reserved tokens: ", token.reservedUntilTokenId());
console2.log("Total ownership percentage of founders: ", token.totalFounderOwnership());
console2.log("Percentage of the first founder: ", token.getFounder(0).ownershipPct);
console2.log("Percentage of the second founder: ", token.getFounder(1).ownershipPct);
address walletFounder1 = token.getFounder(0).wallet;
address walletFounder2 = token.getFounder(1).wallet;
console2.log("Balance of the first founder: ", token.balanceOf(walletFounder1));
console2.log("Balance of the compromised founder: ", token.balanceOf(walletFounder2));
console2.log("Here is the total supply: ", token.totalSupply());
}
Logs:
From Token.sol _addFounders: 100
From Token.sol _addFounders: 20
From Token.sol _addFounders: 40
From Token.sol _addFounders: 60
From Token.sol _addFounders: 80
From Token.sol _addFounders: 1
From Token.sol _addFounders: 21
From Token.sol _addFounders: 41
From Token.sol _addFounders: 61
From Token.sol _addFounders: 81
Number of reserved tokens: 100
Total ownership percentage of founders: 10
Percentage of the first founder: 5
Percentage of the second founder: 5
Balance of the first founder: 22
Balance of the compromised founder: 28
Here is the total supply: 550
To run the test use: forge test -vvv --mt test_FounderLosesNFTShare
Impact
In the above POC we set the reservedUntilTokenId to 100, meaning that there will be 100 NFTs that can be minted by whitelisted users. We also set two founders both with 5% share of the total NFTs minted. As can be seen from the above POC the first founder receives much less NFTs than the second founder. The NFTs are minted by regular users, winning the auction for the certain NFT. It is possible that founders catch up on the fact that they are not receiving the correct amount of NFTs they are entitled to, and may try to fix this by either first calling setReservedUntilTokenId() function in order to set a different reservedUntilTokenId, and then calling the updateFounders() function, or directly calling the updateFounders() function with some parameters that will (e.g try and distribute an NFT to another address held by the founder that is not receiving the full % of NFTs entitled to him). The updateFounders() function is implemented incorrectly, but this is a separate issue. The setReservedUntilTokenId() function can only be called if there are no NFTs minted by normal users via winning an auction. Even if they find the correct parameters to execute some of the above mentioned flows which is highly unlikely, they would have still lost some of the NFTs entitled to them, until that moment. From the below _addFounders() function we can see that the first time uint256 baseTokenId = reservedUntilTokenId is reached, when reservedUntilTokenId is 100 as in the above provided POC baseTokenId will be equal to 100.
function _addFounders(IManager.FounderParams[] calldata_founders, uint256reservedUntilTokenId) internal {
// Used to store the total percent ownership among the foundersuint256 totalOwnership;
uint8 numFoundersAdded =0;
unchecked {
// For each founder:for (uint256 i; i < _founders.length; ++i) {
// ...// Compute the vesting scheduleuint256 schedule =100/ founderPct;
// Used to store the base token id the founder will recieveuint256 baseTokenId = reservedUntilTokenId; //@audit in the above provided POC this will be set to 100// For each token to vest:for (uint256 j; j < founderPct; ++j) {
// Get the available token id
baseTokenId =_getNextTokenId(baseTokenId); //@audit _getNExtTokenId is called with 100 as param// Store the founder as the recipient
tokenRecipient[baseTokenId] = newFounder;
emitMintScheduled(baseTokenId, founderId, newFounder);
// Update the base token id
baseTokenId = (baseTokenId + schedule) %100;
}
}
//...
}
}
Then the function enters in a for loop and calls _getNextTokenId(baseTokenId) with 100 as a parameter. The while loop in _getNextTokenId() won't execute as this is the first call to it , and there is nothing set in the tokenRecipient mapping. Thus this function will return 100 as the _tokenId. In the _addFounders() function we then see that the tokenRecipient mapping is updated with the parameters of the founder: tokenRecipient[baseTokenId] = newFounder;
function _getNextTokenId(uint256_tokenId) internalviewreturns (uint256) {
unchecked {
while (tokenRecipient[_tokenId].wallet !=address(0)) {
_tokenId = (++_tokenId) %100;
}
return _tokenId;
}
}
When the Auction.sol contract creates an auction it calls the mint() function in the Token.sol contract
function _mintWithVesting(addressrecipient) internalreturns (uint256tokenId) {
// Cannot realistically overflowunchecked {
do {
// Get the next token to mint
tokenId = reservedUntilTokenId + settings.mintCount++;
// Lookup whether the token is for a founder, and mint accordingly if so
} while (_isForFounder(tokenId));
}
// Mint the next available token to the recipient for bidding_mint(recipient, tokenId);
}
which in turn calls _isForFounder in order to check if an NFT should be minted to a founder. baseTokenId is calculated by applying the modulo operator to the current _tokenId. As described above we have a tokenRecipient[100] mapping pointing to the registered founder parameters. The problem is that there is no positive integer that when divided by 100 has a remainder equal to 100 or more. Meaning that baseTokenId can't be equal to 100 and the first if condition will be triggered. When the correct flow would be to trigger the else if condition, and mint an NFT to the founder if he is still vesting. In the above POC when the ownership percentage of the founder is 5 he will lose 20% of the NFTs entitled to him.
function _isForFounder(uint256_tokenId) privatereturns (bool) {
// Get the base token iduint256 baseTokenId = _tokenId %100;
// If there is no scheduled recipient:if (tokenRecipient[baseTokenId].wallet ==address(0)) {
returnfalse;
// Else if the founder is still vesting:
} elseif (block.timestamp< tokenRecipient[baseTokenId].vestExpiry) {
// Mint the token to the founder_mint(tokenRecipient[baseTokenId].wallet, _tokenId);
returntrue;
// Else the founder has finished vesting:
}
// ...
}
Code Snippet
for (uint256 j; j < founderPct; ++j) {
// Get the available token id
baseTokenId =_getNextTokenId(baseTokenId); //@audit _getNExtTokenId is called with 100 as param// Store the founder as the recipient
tokenRecipient[baseTokenId] = newFounder;
emitMintScheduled(baseTokenId, founderId, newFounder);
// Update the base token id
baseTokenId = (baseTokenId + schedule) %100;
}
Tool used
Manual Review & Foundry
Recommendation
In _addFounders() function consider applying the %100operation to baseTokenId before the first call to _getNextTokenId() function is made.
sherlock-admin
changed the title
Damp Fern Ant - Reserving 100 or more tokens for claiming when a DAO is created results in the first founder not receiving % of the NFTs entitled to him.
dimulski - Reserving 100 or more tokens for claiming when a DAO is created results in the first founder not receiving % of the NFTs entitled to him.
Dec 13, 2023
Sign up for freeto subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Labels
DuplicateA valid issue that is a duplicate of an issue with `Has Duplicates` labelHighA valid High severity issueRewardA payout will be made for this issue
dimulski
medium
Reserving
100
or more tokens for claiming when a DAO is created results in the first founder not receiving % of the NFTs entitled to him.Summary
When a DAO is created there are
n
number of tokens that can be reserved for a private sale, if the founders have decided to bootstrap the project in such a way, or mint NFTs to certain people on discount. When setting up the parameters for the DAO there is the option for different founders to be set whom in turn will receive certain percentage of all the minted NFTs, until their vesting period is over. NFTs grant voting power. For example Nouns DAO mints1
of each10 NFTs
to the treasury, a single auction continues for a day. In another scenario auctions can also be held every15 mins
for example Lil Nouns. The problem arises ifreservedUntilTokenId
inToken.sol
is set to100
or more. NFTs are distributed to founders based on their ownership percentage. For example if the total ownership of all the founders is10%
, and regular users mint100 NFTs
, the total minted NFTs will be110
and10 NFTs
will be distributed to the founders based on their percentage share. Keep in mind that creators of DAOs on the nounsbuilder protocol may choose to reserve a lot of tokens based on factors such as the duration of each auction, for example as mentioned above if auctions are held every15 mins
we can assume that the reserve tokens will be way above100
.Note: This issue doesn't describe a malicious owner imputing parameters that will benefit him, or supplying a specific wrong parameter that triggers an edge case which in turns leads to a loss of value for the protocol or founders. This issue describes a scenario where an admin or governance enters a parameter that would be expected under normal conditions, and something is broken.
Vulnerability Detail
First add the following to
Token.t.sol
In
NounsBuilderTest.sol
modify the following:96
changepercents[0] = 10;
topercents[0] = 5;
131
change0
to100
To better illustrate what is happening in the
_addFounders()
function add the following to theToken.sol
contract:170
in the_addFounders()
function addconsole2.log("From Token.sol _addFounders: ", baseTokenId);
Add the following test to
Token.t.sol
To run the test use:
forge test -vvv --mt test_FounderLosesNFTShare
Impact
In the above POC we set the
reservedUntilTokenId
to100
, meaning that there will be 100 NFTs that can be minted by whitelisted users. We also set two founders both with 5% share of the total NFTs minted. As can be seen from the above POC the first founder receives much less NFTs than the second founder. The NFTs are minted by regular users, winning the auction for the certain NFT. It is possible that founders catch up on the fact that they are not receiving the correct amount of NFTs they are entitled to, and may try to fix this by either first callingsetReservedUntilTokenId()
function in order to set a differentreservedUntilTokenId
, and then calling theupdateFounders()
function, or directly calling theupdateFounders()
function with some parameters that will (e.g try and distribute an NFT to another address held by the founder that is not receiving the full % of NFTs entitled to him). TheupdateFounders()
function is implemented incorrectly, but this is a separate issue. ThesetReservedUntilTokenId()
function can only be called if there are no NFTs minted by normal users via winning an auction. Even if they find the correct parameters to execute some of the above mentioned flows which is highly unlikely, they would have still lost some of the NFTs entitled to them, until that moment. From the below_addFounders()
function we can see that the first timeuint256 baseTokenId = reservedUntilTokenId
is reached, whenreservedUntilTokenId
is100
as in the above provided POCbaseTokenId
will be equal to100
.Then the function enters in a for loop and calls
_getNextTokenId(baseTokenId)
with100
as a parameter. The while loop in_getNextTokenId()
won't execute as this is the first call to it , and there is nothing set in thetokenRecipient
mapping. Thus this function will return100
as the_tokenId
. In the_addFounders()
function we then see that thetokenRecipient
mapping is updated with the parameters of the founder:tokenRecipient[baseTokenId] = newFounder;
When the
Auction.sol
contract creates an auction it calls themint()
function in theToken.sol
contractwhich in turn calls
_mintWithVesting()
which in turn calls
_isForFounder
in order to check if an NFT should be minted to a founder.baseTokenId
is calculated by applying the modulo operator to the current_tokenId
. As described above we have atokenRecipient[100]
mapping pointing to the registered founder parameters. The problem is that there is no positive integer that when divided by100
has a remainder equal to100
or more. Meaning thatbaseTokenId
can't be equal to100
and the first if condition will be triggered. When the correct flow would be to trigger the else if condition, and mint an NFT to the founder if he is still vesting. In the above POC when the ownership percentage of the founder is5
he will lose20%
of the NFTs entitled to him.Code Snippet
Tool used
Manual Review & Foundry
Recommendation
In
_addFounders()
function consider applying the%100
operation tobaseTokenId
before the first call to_getNextTokenId()
function is made.Duplicate of #42
The text was updated successfully, but these errors were encountered: