Skip to content

Commit be6f40a

Browse files
authored
Audit fixes (#173)
* [M-1] The SequentialTokenIdERC1155 module fails to apply the correct tokenId when installed after initial minting - updated sequentialTokenIdERC1155 to now be able to initialize nextTokenId * [H-1] ClaimableERC20 and MintableERC20 modules incorrectly handle tokens with decimals other than 18 - uses IERC20.decimals instead of 1e18 * [H-2] Claimable modules lead to storage collisions when being updgraded * [H-3] BatchMetadata modules may apply baseURI to incorrect token ids * [Q-1] FallbackFunction array of Claimable modules can be reduced * [Q-2] Nitpicks
1 parent 8f45917 commit be6f40a

16 files changed

+370
-199
lines changed

Diff for: src/callback/BeforeMintCallbackERC721.sol

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ contract BeforeMintCallbackERC721 {
1717
* @notice The beforeMintERC721 hook that is called by a core token before minting tokens.
1818
*
1919
* @param _to The address that is minting tokens.
20+
* @param _startTokenId The token ID being minted.
2021
* @param _amount The amount of tokens to mint.
2122
* @param _data Optional extra data passed to the hook.
2223
* @return result Abi encoded bytes result of the hook.

Diff for: src/callback/BeforeMintWithSignatureCallbackERC1155.sol

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ contract BeforeMintWithSignatureCallbackERC1155 {
1717
* @notice The beforeMintWithSignatureERC1155 hook that is called by a core token before minting tokens.
1818
*
1919
* @param _to The address that is minting tokens.
20+
* @param _id The token ID being minted.
2021
* @param _amount The quantity of tokens to mint.
2122
* @param _data Optional extra data passed to the hook.
2223
* @param _signer The address that signed the minting request.

Diff for: src/callback/BeforeMintWithSignatureCallbackERC721.sol

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ contract BeforeMintWithSignatureCallbackERC721 {
1717
* @notice The beforeMintWithSignatureERC721 hook that is called by a core token before minting tokens.
1818
*
1919
* @param _to The address that is minting tokens.
20+
* @param _startTokenId The token ID being minted.
2021
* @param _amount The amount of tokens to mint.
2122
* @param _data Optional extra data passed to the hook.
2223
* @param _signer The address that signed the minting request.

Diff for: src/core/token/ERC20Base.sol

+4
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,10 @@ contract ERC20Base is ERC20, Multicallable, Core, EIP712 {
234234
* @param owner The account approving the tokens
235235
* @param spender The address to approve
236236
* @param amount Amount of tokens to approve
237+
* @param deadline Deadline after which the approval is no longer valid
238+
* @param v Signature param
239+
* @param r Signature param
240+
* @param s Signature param
237241
*/
238242
function permit(address owner, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
239243
public

Diff for: src/module/token/metadata/BatchMetadataERC1155.sol

+4-4
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ contract BatchMetadataERC1155 is BatchMetadataERC721, UpdateMetadataCallbackERC1
2222
FallbackFunction({selector: this.setBaseURI.selector, permissionBits: Role._MANAGER_ROLE});
2323
config.fallbackFunctions[2] =
2424
FallbackFunction({selector: this.getAllMetadataBatches.selector, permissionBits: 0});
25-
config.fallbackFunctions[3] = FallbackFunction({selector: this.nextTokenIdToMint.selector, permissionBits: 0});
26-
config.fallbackFunctions[4] = FallbackFunction({selector: this.getBatchId.selector, permissionBits: 0});
27-
config.fallbackFunctions[5] = FallbackFunction({selector: this.getBatchRange.selector, permissionBits: 0});
25+
config.fallbackFunctions[3] = FallbackFunction({selector: this.getMetadataBatch.selector, permissionBits: 0});
26+
config.fallbackFunctions[4] = FallbackFunction({selector: this.nextTokenIdToMint.selector, permissionBits: 0});
27+
config.fallbackFunctions[5] = FallbackFunction({selector: this.getBatchIndex.selector, permissionBits: 0});
2828

2929
config.requiredInterfaces = new bytes4[](1);
3030
config.requiredInterfaces[0] = 0xd9b67a26; // ERC1155
@@ -44,7 +44,7 @@ contract BatchMetadataERC1155 is BatchMetadataERC721, UpdateMetadataCallbackERC1
4444
if (_startTokenId < _batchMetadataStorage().nextTokenIdRangeStart) {
4545
revert BatchMetadataMetadataAlreadySet();
4646
}
47-
_setMetadata(_quantity, _baseURI);
47+
_setMetadata(_startTokenId, _quantity, _baseURI);
4848
}
4949

5050
}

Diff for: src/module/token/metadata/BatchMetadataERC721.sol

+39-71
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,10 @@ library BatchMetadataStorage {
1414
keccak256(abi.encode(uint256(keccak256("token.metadata.batch")) - 1)) & ~bytes32(uint256(0xff));
1515

1616
struct Data {
17-
// tokenId range end
18-
uint256[] tokenIdRangeEnd;
1917
// next tokenId as range start
2018
uint256 nextTokenIdRangeStart;
21-
// tokenId range end => baseURI of range
22-
mapping(uint256 => string) baseURIOfTokenIdRange;
19+
// metadata batches
20+
BatchMetadataERC721.MetadataBatch[] metadataBatches;
2321
}
2422

2523
function data() internal pure returns (Data storage data_) {
@@ -42,7 +40,7 @@ contract BatchMetadataERC721 is Module, UpdateMetadataCallbackERC721 {
4240
/**
4341
* @notice MetadataBatch struct to store metadata for a range of tokenIds.
4442
* @param startTokenIdInclusive The first tokenId in the range.
45-
* @param endTokenIdNonInclusive The last tokenId in the range.
43+
* @param endTokenIdInclusive The last tokenId in the range.
4644
* @param baseURI The base URI for the range.
4745
*/
4846
struct MetadataBatch {
@@ -69,7 +67,7 @@ contract BatchMetadataERC721 is Module, UpdateMetadataCallbackERC721 {
6967
//////////////////////////////////////////////////////////////*/
7068

7169
/// @dev ERC-4906 Metadata Update.
72-
event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId);
70+
event BatchMetadataUpdate(uint256 startTokenIdIncluside, uint256 endTokenIdInclusive, string baseURI);
7371

7472
/*//////////////////////////////////////////////////////////////
7573
MODULE CONFIG
@@ -89,9 +87,9 @@ contract BatchMetadataERC721 is Module, UpdateMetadataCallbackERC721 {
8987
FallbackFunction({selector: this.setBaseURI.selector, permissionBits: Role._MANAGER_ROLE});
9088
config.fallbackFunctions[2] =
9189
FallbackFunction({selector: this.getAllMetadataBatches.selector, permissionBits: 0});
92-
config.fallbackFunctions[3] = FallbackFunction({selector: this.nextTokenIdToMint.selector, permissionBits: 0});
93-
config.fallbackFunctions[4] = FallbackFunction({selector: this.getBatchId.selector, permissionBits: 0});
94-
config.fallbackFunctions[5] = FallbackFunction({selector: this.getBatchRange.selector, permissionBits: 0});
90+
config.fallbackFunctions[3] = FallbackFunction({selector: this.getMetadataBatch.selector, permissionBits: 0});
91+
config.fallbackFunctions[4] = FallbackFunction({selector: this.nextTokenIdToMint.selector, permissionBits: 0});
92+
config.fallbackFunctions[5] = FallbackFunction({selector: this.getBatchIndex.selector, permissionBits: 0});
9593

9694
config.requiredInterfaces = new bytes4[](1);
9795
config.requiredInterfaces[0] = 0x80ac58cd; // ERC721.
@@ -121,7 +119,7 @@ contract BatchMetadataERC721 is Module, UpdateMetadataCallbackERC721 {
121119
if (_startTokenId < _batchMetadataStorage().nextTokenIdRangeStart) {
122120
revert BatchMetadataMetadataAlreadySet();
123121
}
124-
_setMetadata(_quantity, _baseURI);
122+
_setMetadata(_startTokenId, _quantity, _baseURI);
125123
}
126124

127125
/*//////////////////////////////////////////////////////////////
@@ -130,71 +128,42 @@ contract BatchMetadataERC721 is Module, UpdateMetadataCallbackERC721 {
130128

131129
/// @notice Returns all metadata batches for a token.
132130
function getAllMetadataBatches() external view returns (MetadataBatch[] memory) {
133-
uint256[] memory rangeEnds = _batchMetadataStorage().tokenIdRangeEnd;
134-
uint256 numOfBatches = rangeEnds.length;
135-
136-
MetadataBatch[] memory batches = new MetadataBatch[](rangeEnds.length);
137-
138-
uint256 rangeStart = 0;
139-
for (uint256 i = 0; i < numOfBatches; i += 1) {
140-
batches[i] = MetadataBatch({
141-
startTokenIdInclusive: rangeStart,
142-
endTokenIdInclusive: rangeEnds[i] - 1,
143-
baseURI: _batchMetadataStorage().baseURIOfTokenIdRange[rangeEnds[i]]
144-
});
145-
rangeStart = rangeEnds[i];
146-
}
131+
return _batchMetadataStorage().metadataBatches;
132+
}
147133

148-
return batches;
134+
/// @dev returns the metadata batch for a given batchIndex
135+
function getMetadataBatch(uint256 _batchIndex) public view returns (MetadataBatch memory) {
136+
return _batchMetadataStorage().metadataBatches[_batchIndex];
149137
}
150138

151139
/// @notice Uploads metadata for a range of tokenIds.
152140
function uploadMetadata(uint256 _amount, string calldata _baseURI) external virtual {
153-
_setMetadata(_amount, _baseURI);
141+
_setMetadata(_batchMetadataStorage().nextTokenIdRangeStart, _amount, _baseURI);
154142
}
155143

156144
function nextTokenIdToMint() external view returns (uint256) {
157145
return _batchMetadataStorage().nextTokenIdRangeStart;
158146
}
159147

160-
/// @dev Returns the id for the batch of tokens the given tokenId belongs to.
161-
function getBatchId(uint256 _tokenId) public view virtual returns (uint256 batchId, uint256 index) {
162-
uint256[] memory rangeEnds = _batchMetadataStorage().tokenIdRangeEnd;
163-
uint256 numOfBatches = rangeEnds.length;
164-
165-
for (uint256 i = 0; i < numOfBatches; i += 1) {
166-
if (_tokenId < rangeEnds[i]) {
167-
index = i;
168-
batchId = rangeEnds[i];
169-
170-
return (batchId, index);
171-
}
172-
}
173-
revert BatchMetadataNoMetadataForTokenId();
174-
}
175-
176-
/// @dev returns the starting tokenId of a given batchId.
177-
function getBatchRange(uint256 _batchID) public view returns (uint256, uint256) {
178-
uint256[] memory rangeEnds = _batchMetadataStorage().tokenIdRangeEnd;
179-
uint256 numOfBatches = rangeEnds.length;
148+
/// @dev Returns the index for the batch of tokens the given tokenId belongs to.
149+
function getBatchIndex(uint256 _tokenId) public view virtual returns (uint256) {
150+
MetadataBatch[] memory batches = _batchMetadataStorage().metadataBatches;
151+
uint256 numOfBatches = batches.length;
180152

181153
for (uint256 i = 0; i < numOfBatches; i += 1) {
182-
if (_batchID == rangeEnds[i]) {
183-
if (i > 0) {
184-
return (rangeEnds[i - 1], rangeEnds[i] - 1);
185-
}
186-
return (0, rangeEnds[i] - 1);
154+
if (_tokenId >= batches[i].startTokenIdInclusive && _tokenId <= batches[i].endTokenIdInclusive) {
155+
return i;
187156
}
188157
}
189-
190158
revert BatchMetadataNoMetadataForTokenId();
191159
}
192160

193-
/// @dev Sets the base URI for the batch of tokens with the given batchId.
194-
function setBaseURI(uint256 _batchId, string memory _baseURI) external virtual {
195-
_batchMetadataStorage().baseURIOfTokenIdRange[_batchId] = _baseURI;
196-
(uint256 startTokenId,) = getBatchRange(_batchId);
197-
emit BatchMetadataUpdate(startTokenId, _batchId);
161+
/// @dev Sets the base URI for the batch based on the batchIndex.
162+
function setBaseURI(uint256 _batchIndex, string memory _baseURI) external virtual {
163+
MetadataBatch memory batch = _batchMetadataStorage().metadataBatches[_batchIndex];
164+
batch.baseURI = _baseURI;
165+
_batchMetadataStorage().metadataBatches[_batchIndex] = batch;
166+
emit BatchMetadataUpdate(batch.startTokenIdInclusive, batch.endTokenIdInclusive, batch.baseURI);
198167
}
199168

200169
/*//////////////////////////////////////////////////////////////
@@ -203,35 +172,34 @@ contract BatchMetadataERC721 is Module, UpdateMetadataCallbackERC721 {
203172

204173
/// @dev Returns the baseURI for a token. The intended metadata URI for the token is baseURI + indexInBatch.
205174
function _getBaseURI(uint256 _tokenId) internal view returns (string memory baseUri, uint256 indexInBatch) {
206-
uint256[] memory rangeEnds = _batchMetadataStorage().tokenIdRangeEnd;
207-
uint256 numOfBatches = rangeEnds.length;
175+
BatchMetadataERC721.MetadataBatch[] memory batches = _batchMetadataStorage().metadataBatches;
176+
uint256 numOfBatches = batches.length;
208177

209178
for (uint256 i = 0; i < numOfBatches; i += 1) {
210-
if (_tokenId < rangeEnds[i]) {
211-
uint256 rangeStart = 0;
212-
if (i > 0) {
213-
rangeStart = rangeEnds[i - 1];
214-
}
215-
return (_batchMetadataStorage().baseURIOfTokenIdRange[rangeEnds[i]], _tokenId - rangeStart);
179+
if (_tokenId >= batches[i].startTokenIdInclusive && _tokenId <= batches[i].endTokenIdInclusive) {
180+
return (batches[i].baseURI, _tokenId - batches[i].startTokenIdInclusive);
216181
}
217182
}
218183
revert BatchMetadataNoMetadataForTokenId();
219184
}
220185

221186
/// @notice sets the metadata for a range of tokenIds.
222-
function _setMetadata(uint256 _amount, string calldata _baseURI) internal virtual {
187+
function _setMetadata(uint256 _rangeStart, uint256 _amount, string calldata _baseURI) internal virtual {
223188
if (_amount == 0) {
224189
revert BatchMetadataZeroAmount();
225190
}
226191

227-
uint256 rangeStart = _batchMetadataStorage().nextTokenIdRangeStart;
228-
uint256 rangeEndNonInclusive = rangeStart + _amount;
192+
uint256 rangeEndNonInclusive = _rangeStart + _amount;
229193

194+
MetadataBatch memory batch = MetadataBatch({
195+
startTokenIdInclusive: _rangeStart,
196+
endTokenIdInclusive: rangeEndNonInclusive - 1,
197+
baseURI: _baseURI
198+
});
199+
_batchMetadataStorage().metadataBatches.push(batch);
230200
_batchMetadataStorage().nextTokenIdRangeStart = rangeEndNonInclusive;
231-
_batchMetadataStorage().tokenIdRangeEnd.push(rangeEndNonInclusive);
232-
_batchMetadataStorage().baseURIOfTokenIdRange[rangeEndNonInclusive] = _baseURI;
233201

234-
emit BatchMetadataUpdate(rangeStart, rangeEndNonInclusive - 1);
202+
emit BatchMetadataUpdate(_rangeStart, rangeEndNonInclusive - 1, _baseURI);
235203
}
236204

237205
function _batchMetadataStorage() internal pure returns (BatchMetadataStorage.Data storage) {

Diff for: src/module/token/minting/ClaimableERC1155.sol

+5-5
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ library ClaimableStorage {
1919
keccak256(abi.encode(uint256(keccak256("token.minting.claimable.erc1155")) - 1)) & ~bytes32(uint256(0xff));
2020

2121
struct Data {
22-
// sale config: primary sale recipient, and platform fee recipient + BPS.
23-
ClaimableERC1155.SaleConfig saleConfig;
24-
// token ID => claim condition
25-
mapping(uint256 => ClaimableERC1155.ClaimCondition) claimConditionByTokenId;
2622
// UID => whether it has been used
2723
mapping(bytes32 => bool) uidUsed;
2824
// address => uint256 => how many tokens have been minted
2925
mapping(address => mapping(uint256 => uint256)) totalMinted;
26+
// sale config: primary sale recipient, and platform fee recipient + BPS.
27+
ClaimableERC1155.SaleConfig saleConfig;
28+
// token ID => claim condition
29+
mapping(uint256 => ClaimableERC1155.ClaimCondition) claimConditionByTokenId;
3030
}
3131

3232
function data() internal pure returns (Data storage data_) {
@@ -161,7 +161,7 @@ contract ClaimableERC1155 is
161161
/// @notice Returns all implemented callback and fallback functions.
162162
function getModuleConfig() external pure override returns (ModuleConfig memory config) {
163163
config.callbackFunctions = new CallbackFunction[](2);
164-
config.fallbackFunctions = new FallbackFunction[](5);
164+
config.fallbackFunctions = new FallbackFunction[](4);
165165

166166
config.callbackFunctions[0] = CallbackFunction(this.beforeMintERC1155.selector);
167167
config.callbackFunctions[1] = CallbackFunction(this.beforeMintWithSignatureERC1155.selector);

Diff for: src/module/token/minting/ClaimableERC20.sol

+23-12
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pragma solidity ^0.8.20;
44
import {Module} from "../../../Module.sol";
55

66
import {Role} from "../../../Role.sol";
7+
78
import {IInstallationCallback} from "../../../interface/IInstallationCallback.sol";
89
import {OwnableRoles} from "@solady/auth/OwnableRoles.sol";
910
import {MerkleProofLib} from "@solady/utils/MerkleProofLib.sol";
@@ -12,21 +13,27 @@ import {SafeTransferLib} from "@solady/utils/SafeTransferLib.sol";
1213
import {BeforeMintCallbackERC20} from "../../../callback/BeforeMintCallbackERC20.sol";
1314
import {BeforeMintWithSignatureCallbackERC20} from "../../../callback/BeforeMintWithSignatureCallbackERC20.sol";
1415

16+
interface IERC20 {
17+
18+
function decimals() external view returns (uint8);
19+
20+
}
21+
1522
library ClaimableStorage {
1623

1724
/// @custom:storage-location erc7201:token.minting.claimable.erc20
1825
bytes32 public constant CLAIMABLE_STORAGE_POSITION =
1926
keccak256(abi.encode(uint256(keccak256("token.minting.claimable.erc20")) - 1)) & ~bytes32(uint256(0xff));
2027

2128
struct Data {
22-
// sale config: primary sale recipient, and platform fee recipient + BPS.
23-
ClaimableERC20.SaleConfig saleConfig;
24-
// claim condition
25-
ClaimableERC20.ClaimCondition claimCondition;
2629
// UID => whether it has been used
2730
mapping(bytes32 => bool) uidUsed;
2831
// address => how many tokens have been minted
2932
mapping(address => uint256) totalMinted;
33+
// sale config: primary sale recipient, and platform fee recipient + BPS.
34+
ClaimableERC20.SaleConfig saleConfig;
35+
// claim condition
36+
ClaimableERC20.ClaimCondition claimCondition;
3037
}
3138

3239
function data() internal pure returns (Data storage data_) {
@@ -83,9 +90,8 @@ contract ClaimableERC20 is
8390
*
8491
* @param startTimestamp The timestamp at which the minting request is valid.
8592
* @param endTimestamp The timestamp at which the minting request expires.
86-
* @param recipient The address that will receive the minted tokens.
87-
* @param amount The amount of tokens to mint.
8893
* @param currency The address of the currency used to pay for the minted tokens.
94+
* @param maxMintPerWallet The maximum number of tokens that can be minted per wallet.
8995
* @param pricePerUnit The price per unit of the minted tokens.
9096
* @param uid A unique identifier for the minting request.
9197
*/
@@ -101,8 +107,9 @@ contract ClaimableERC20 is
101107
/**
102108
* @notice The parameters sent to the `beforeMintERC20` callback function.
103109
*
104-
* @param request The minting request.
105-
* @param signature The signature produced from signing the minting request.
110+
* @param currency The address of the currency used to pay for the minted tokens.
111+
* @param pricePerUnit The price per unit of the minted tokens.
112+
* @param recipientAllowlistProof The proof of the recipient's address in the allowlist.
106113
*/
107114
struct ClaimParamsERC20 {
108115
address currency;
@@ -160,7 +167,7 @@ contract ClaimableERC20 is
160167
/// @notice Returns all implemented callback and fallback functions.
161168
function getModuleConfig() external pure override returns (ModuleConfig memory config) {
162169
config.callbackFunctions = new CallbackFunction[](2);
163-
config.fallbackFunctions = new FallbackFunction[](5);
170+
config.fallbackFunctions = new FallbackFunction[](4);
164171

165172
config.callbackFunctions[0] = CallbackFunction(this.beforeMintERC20.selector);
166173
config.callbackFunctions[1] = CallbackFunction(this.beforeMintWithSignatureERC20.selector);
@@ -194,10 +201,12 @@ contract ClaimableERC20 is
194201

195202
_validateClaimCondition(_to, _amount, _params.currency, _params.pricePerUnit, _params.recipientAllowlistProof);
196203

197-
_distributeMintPrice(msg.sender, _params.currency, (_amount * _params.pricePerUnit) / 1e18);
204+
_distributeMintPrice(
205+
msg.sender, _params.currency, (_amount * _params.pricePerUnit) / (10 ** IERC20(address(this)).decimals())
206+
);
198207
}
199208

200-
/// @notice Callback function for the ERC20Core.mint function.
209+
/// @notice Callback function for the ERC20Core.mintWithSignature function.
201210
function beforeMintWithSignatureERC20(address _to, uint256 _amount, bytes memory _data, address _signer)
202211
external
203212
payable
@@ -213,7 +222,9 @@ contract ClaimableERC20 is
213222

214223
_validateClaimSignatureParams(_params, _to, _amount);
215224

216-
_distributeMintPrice(msg.sender, _params.currency, (_amount * _params.pricePerUnit) / 1e18);
225+
_distributeMintPrice(
226+
msg.sender, _params.currency, (_amount * _params.pricePerUnit) / (10 ** IERC20(address(this)).decimals())
227+
);
217228
}
218229

219230
/// @dev Called by a Core into an Module during the installation of the Module.

0 commit comments

Comments
 (0)