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.
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
Invariant Violation: Token.sol#_updateFounders does not respect reservedUntilTokenId.
Summary
When adding new founders to a Token.sol, it is possible to define a reservedUntilTokenId to define what index auctions will start at.
However, when we make a call to updateFounders, it does not take into account the offset correctly and will fail to remove existing founders. This leads to unintentional founder mints and direct subversion of the protocol's maximum limit on founder ownership.
Vulnerability Detail
When initializing the mapping of founders via a call to _addFounders, the token distribution schedule is calculated taking into account a reservedUntilTokenId offset. When this value is non-zero, this has the effect of shifting the address generation schedule by an equivalent number of tokenIds.
For visual demonstration, for a single founder with 1% of ownership and no reserved tokens (Sequence A), the following token distribution pattern is used:
Token ID
Founder Reservation Address
1
0x0000000000000000000000000000000000000069
2
0x0000000000000000000000000000000000000000
...
...
101
0x0000000000000000000000000000000000000069
102
0x0000000000000000000000000000000000000000
Next, let's imagine a founder with 1% ownership and a single reserved token (Sequence B):
Token ID
Founder Reservation Address
1
0x0000000000000000000000000000000000000000
2
0x0000000000000000000000000000000000000069
...
...
101
0x0000000000000000000000000000000000000000
102
0x0000000000000000000000000000000000000069
Notice the generation sequence is effectively reversed.
As a result, if we try to assign token distribution Sequence B with Sequence A via a call to updateFounders, the resultant token recipients are as follows:
Token ID
Founder Reservation Address (Perpetual Vesting)
1
0x0000000000000000000000000000000000000069
2
0x0000000000000000000000000000000000000069
...
0x0000000000000000000000000000000000000069
101
0x0000000000000000000000000000000000000069
102
0x0000000000000000000000000000000000000069
...
...
type(uint256).max
0x0000000000000000000000000000000000000069
Notice that this has the effect of completely ignoring any of the previously reserved tokenIds whilst trying to purge the old founder configuration.
This can be reproduced in Foundry using the following test:
Notice here that both address(0x94) and address(0x97) are rewarded as founders after the call to updateFounders, even though we have asserted that totalFounders is 1.
Impact
Addresses which should not be rewarded as founders will continue to be rewarded.
The totalOwnership is not correctly adhered to, as there are "hidden founders" who accept a greater amount of ownership than explicitly intended.
/// @notice Update the list of allocation owners/// @param newFounders the full list of foundersfunction updateFounders(IManager.FounderParams[] calldatanewFounders) external onlyOwner {
// Cache the number of foundersuint256 numFounders = settings.numFounders;
// Get a temporary array to hold all founders
Founder[] memory cachedFounders =newFounder[](numFounders);
// Cannot realistically overflowunchecked {
// Add each founder to the arrayfor (uint256 i; i < numFounders; ++i) {
cachedFounders[i] = founder[i];
}
}
// Keep a mapping of all the reserved token IDs we're set to clear.bool[] memory clearedTokenIds =newbool[](100);
unchecked {
// for each existing founder:for (uint256 i; i < cachedFounders.length; ++i) {
// copy the founder into memory
Founder memory cachedFounder = cachedFounders[i];
// Delete the founder from the stored mappingdelete founder[i];
// Some DAOs were initialized with 0 percentage ownership.// This skips them to avoid a division by zero error.if (cachedFounder.ownershipPct ==0) {
continue;
}
// using the ownership percentage, get reserved token percentagesuint256 schedule =100/ cachedFounder.ownershipPct;
// Used to reverse engineer the indices the founder has reserved tokens in.uint256 baseTokenId;
for (uint256 j; j < cachedFounder.ownershipPct; ++j) {
// Get the next index that hasn't already been clearedwhile (clearedTokenIds[baseTokenId] !=false) {
baseTokenId = (++baseTokenId) %100;
}
delete tokenRecipient[baseTokenId];
clearedTokenIds[baseTokenId] =true;
emitMintUnscheduled(baseTokenId, i, cachedFounder);
// Update the base token id
baseTokenId = (baseTokenId + schedule) %100;
}
}
}
// Clear values from storage before adding new founders
settings.numFounders =0;
settings.totalOwnership =0;
emitFounderAllocationsCleared(newFounders);
_addFounders(newFounders, reservedUntilTokenId);
}
Tool used
Visual Studio Code, Foundry
Recommendation
A naive solution may recommend to simply suggest to initialize baseTokenId correctly when calling updateFounders:
// Used to reverse engineer the indices the founder has reserved tokens in.
- uint256 baseTokenId;+ uint256 baseTokenId = _reservedUntilTokenId;
However, since this can be changed arbitrarily via calls to setReservedUntilTokenId, it is possible that the process of predicting tokens to unwrap can still be invalidated.
The most secure way to prevent this issue would be to use the modulo operator to our advantage, and merely wrap around the tokenRecipient mapping to zero all entries and prevent possible conflicts:
for (uint256 i =0; i <100;) {
// Delete the founder from the stored mapping (if exists)delete founder[i];
// Delete the preallocated token recipients (if exists)delete tokenRecipient[baseTokenId];
unchecked { i++; }
}
In addition, it is recommended to create additional unit tests which experiment with non-zero _reservedUntilTokenId when making calls to setMockTokenParams.
sherlock-admin
changed the title
Suave Tin Poodle - Invariant Violation: Token.sol#_updateFounders does not respect reservedUntilTokenId.
cawfree - Invariant Violation: Token.sol#_updateFounders does not respect reservedUntilTokenId.
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
cawfree
medium
Invariant Violation:
Token.sol#_updateFounders
does not respectreservedUntilTokenId
.Summary
When adding new founders to a
Token.sol
, it is possible to define areservedUntilTokenId
to define what index auctions will start at.However, when we make a call to
updateFounders
, it does not take into account the offset correctly and will fail to remove existing founders. This leads to unintentional founder mints and direct subversion of the protocol's maximum limit on founder ownership.Vulnerability Detail
When initializing the mapping of
founder
s via a call to_addFounders
, the token distribution schedule is calculated taking into account areservedUntilTokenId
offset. When this value is non-zero, this has the effect of shifting the address generation schedule by an equivalent number oftokenId
s.For visual demonstration, for a single founder with 1% of ownership and no reserved tokens (Sequence A), the following token distribution pattern is used:
0x0000000000000000000000000000000000000069
0x0000000000000000000000000000000000000000
...
0x0000000000000000000000000000000000000069
0x0000000000000000000000000000000000000000
Next, let's imagine a founder with 1% ownership and a single reserved token (Sequence B):
0x0000000000000000000000000000000000000000
0x0000000000000000000000000000000000000069
...
0x0000000000000000000000000000000000000000
0x0000000000000000000000000000000000000069
Notice the generation sequence is effectively reversed.
Now, when making a call to
updateFounders
, thereservedUntilTokenId
is not taken into account when trying to predict the previously allocated addresses. In this regard, when trying to predict the token identifiers possess founder addresses which must to be reset for Sequence B, the codebase mistakenly computes Sequence A instead.As a result, if we try to assign token distribution Sequence B with Sequence A via a call to
updateFounders
, the resultant token recipients are as follows:0x0000000000000000000000000000000000000069
0x0000000000000000000000000000000000000069
0x0000000000000000000000000000000000000069
0x0000000000000000000000000000000000000069
0x0000000000000000000000000000000000000069
type(uint256).max
0x0000000000000000000000000000000000000069
Notice that this has the effect of completely ignoring any of the previously reserved
tokenId
s whilst trying to purge the old founder configuration.This can be reproduced in Foundry using the following test:
Results in the following console output:
Notice here that both
address(0x94)
andaddress(0x97)
are rewarded as founders after the call toupdateFounders
, even though we have asserted thattotalFounders
is1
.Impact
totalOwnership
is not correctly adhered to, as there are "hidden founders" who accept a greater amount of ownership than explicitly intended.totalOwnership <= 99
invariant is defeated.Code Snippet
Tool used
Visual Studio Code, Foundry
Recommendation
A naive solution may recommend to simply suggest to initialize
baseTokenId
correctly when callingupdateFounders
:However, since this can be changed arbitrarily via calls to
setReservedUntilTokenId
, it is possible that the process of predicting tokens to unwrap can still be invalidated.The most secure way to prevent this issue would be to use the modulo operator to our advantage, and merely wrap around the
tokenRecipient
mapping to zero all entries and prevent possible conflicts:In addition, it is recommended to create additional unit tests which experiment with non-zero
_reservedUntilTokenId
when making calls tosetMockTokenParams
.Duplicate of #42
The text was updated successfully, but these errors were encountered: