Skip to content

Commit be12145

Browse files
nkrishangkumaryash90Krishang NadgaudaKrishang Nadgauda
authored
Macro audit june 2022 (#183)
* [H-2] Batch reveal is permanently corrupted by invocation of public getRevealURI method * [L-3] LazyMint of a new batch can affect previous batch * [L-4] Incorrect handling of invalid role approvals/removals in Permissions.sol * [L-5] Incorrect processing of role approval in PermissionsEnumerable.sol * [L-6] claimCondition.startTimestamp is not enforced * docs update * reduce size by merging checks * update tests * [Q-1] Emitted TokensLazyMinted event does not match spec * [L-3] test fix * [Q-3] Some events could use indexed attributes * [Q-5] Visibility for following methods can be changed from public to external * [Q-4] Missing natspec comments * [Q-3] test fixes * forge updates * fix claim quantity limit * add test * run prettier * replace requires with custom errors replace requires with custom errors * standardize errors * update tests * run prettier * [L-6] claimCondition.startTimestamp is not enforced -- revised fix * [L-6] claimCondition.startTimestamp is not enforced * reorganize errors and checks * 0xMacro audit Jun '22: Multiwrap (#181) * [H-1] Wrapped ETH cannot be unwrapped * [L-1] Anyone can call renounceRole(), and delete the first valid entry from roleMembers for any role * [L-2] Incorrect supportsInterface implementation * [G-4] Require revert msg string length > 32 for Multiwrap: L176 (edited) * [G-3] Optimization done in CurrencyTransferLib for rare edge cases, increases gas costs for all other use cases. * [G-2] Improvements for _setBundle() * [L-7] Unsafe usage of msg.value within CurrencyTransfer library * [G-5] Return early in PermissionsEnumerable#getRoleMember Co-authored-by: Krishang Nadgauda <[email protected]> * 0xMacro audit Jun '22: DropERC1155 (#182) * (Q-1) Unused variable * fix claim quantity limit fix claim quantity limit * cleanup * remove thirdwebfee remove thirdwebfee Co-authored-by: Krishang Nadgauda <[email protected]> Co-authored-by: yash <[email protected]> Co-authored-by: Krishang Nadgauda <[email protected]> * custom errors cleanup mega commit * delete commented require statements * fix timestamp check * run prettier * REVERT G-3: breaks Marketplace * Change revert test -> balances test post behaviour fix * Fix faulty >= to > * package bump * package release * add bot check to SigDrop * fix forge build errors * Update permissions usage in pack tests * run prettier * pack docs update Co-authored-by: yash <[email protected]> Co-authored-by: Krishang Nadgauda <[email protected]> Co-authored-by: Krishang Nadgauda <[email protected]>
1 parent cb8d6e5 commit be12145

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+3341
-465
lines changed

Diff for: .DS_Store

8 KB
Binary file not shown.

Diff for: contracts/drop/DropERC1155.sol

+6
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,12 @@ contract DropERC1155 is
597597
emit PrimarySaleRecipientUpdated(_saleRecipient);
598598
}
599599

600+
/// @dev Lets a contract admin set the recipient for all primary sales.
601+
function setSaleRecipientForToken(uint256 _tokenId, address _saleRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) {
602+
saleRecipient[_tokenId] = _saleRecipient;
603+
emit SaleRecipientForTokenUpdated(_tokenId, _saleRecipient);
604+
}
605+
600606
/// @dev Lets a contract admin update the default royalty recipient and bps.
601607
function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps)
602608
external

Diff for: contracts/feature/ContractMetadata.sol

+11-1
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,23 @@ pragma solidity ^0.8.0;
33

44
import "./interface/IContractMetadata.sol";
55

6+
/**
7+
* Thirdweb's `ContractMetadata` is a contract extension for any base contracts. It lets you set a metadata URI
8+
* for you contract.
9+
*
10+
* Additionally, `ContractMetadata` is necessary for NFT contracts that want royalties to get distributed on OpenSea.
11+
*/
12+
613
abstract contract ContractMetadata is IContractMetadata {
714
/// @dev Contract level metadata.
815
string public override contractURI;
916

1017
/// @dev Lets a contract admin set the URI for contract-level metadata.
1118
function setContractURI(string memory _uri) external override {
12-
require(_canSetContractURI(), "Not authorized");
19+
if (!_canSetContractURI()) {
20+
revert ContractMetadata__NotAuthorized();
21+
}
22+
1323
_setupContractURI(_uri);
1424
}
1525

Diff for: contracts/feature/DelayedReveal.sol

+9-5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ pragma solidity ^0.8.0;
33

44
import "./interface/IDelayedReveal.sol";
55

6+
/**
7+
* Thirdweb's `DelayedReveal` is a contract extension for base NFT contracts. It lets you create batches of
8+
* 'delayed-reveal' NFTs. You can learn more about the usage of delayed reveal NFTs here - https://blog.thirdweb.com/delayed-reveal-nfts
9+
*/
10+
611
abstract contract DelayedReveal is IDelayedReveal {
712
/// @dev Mapping from id of a batch of tokens => to encrypted base URI for the respective batch of tokens.
813
mapping(uint256 => bytes) public encryptedBaseURI;
@@ -13,14 +18,13 @@ abstract contract DelayedReveal is IDelayedReveal {
1318
}
1419

1520
/// @dev Returns the decrypted i.e. revealed URI for a batch of tokens.
16-
function getRevealURI(uint256 _batchId, bytes calldata _key) public returns (string memory revealedURI) {
21+
function getRevealURI(uint256 _batchId, bytes calldata _key) public view returns (string memory revealedURI) {
1722
bytes memory encryptedURI = encryptedBaseURI[_batchId];
18-
require(encryptedURI.length != 0, "nothing to reveal.");
23+
if (encryptedURI.length == 0) {
24+
revert DelayedReveal__NothingToReveal(_batchId);
25+
}
1926

2027
revealedURI = string(encryptDecrypt(encryptedURI, _key));
21-
22-
// yash - added this, and removed view mutability
23-
delete encryptedBaseURI[_batchId];
2428
}
2529

2630
/// @dev See: https://ethereum.stackexchange.com/questions/69825/decrypt-message-on-chain

Diff for: contracts/feature/DropSinglePhase.sol

+61-29
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
// SPDX-License-Identifier: Apache-2.0
22
pragma solidity ^0.8.0;
33

4-
import "./interface/IClaimConditionsSinglePhase.sol";
5-
import "./interface/IDrop.sol";
6-
import "./LazyMint.sol";
4+
import "./interface/IDropSinglePhase.sol";
75
import "../lib/MerkleProof.sol";
86
import "../lib/TWBitMaps.sol";
97

10-
abstract contract DropSinglePhase is LazyMint, IDrop, IClaimConditionsSinglePhase {
8+
abstract contract DropSinglePhase is IDropSinglePhase {
119
using TWBitMaps for TWBitMaps.BitMap;
1210

13-
/*///////////////////////////////////////////////////x////////////
11+
/*///////////////////////////////////////////////////////////////
1412
State variables
1513
//////////////////////////////////////////////////////////////*/
1614

@@ -36,6 +34,10 @@ abstract contract DropSinglePhase is LazyMint, IDrop, IClaimConditionsSinglePhas
3634
*/
3735
mapping(bytes32 => TWBitMaps.BitMap) private usedAllowlistSpot;
3836

37+
/*///////////////////////////////////////////////////////////////
38+
Errors
39+
//////////////////////////////////////////////////////////////*/
40+
3941
/*///////////////////////////////////////////////////////////////
4042
Drop logic
4143
//////////////////////////////////////////////////////////////*/
@@ -68,7 +70,10 @@ abstract contract DropSinglePhase is LazyMint, IDrop, IClaimConditionsSinglePhas
6870
);
6971

7072
// Verify claim validity. If not valid, revert.
71-
bool toVerifyMaxQuantityPerTransaction = _allowlistProof.maxQuantityInAllowlist == 0;
73+
// when there's allowlist present --> verifyClaimMerkleProof will verify the maxQuantityInAllowlist value with hashed leaf in the allowlist
74+
// when there's no allowlist, this check is true --> verifyClaim will check for _quantity being equal/less than the limit
75+
bool toVerifyMaxQuantityPerTransaction = _allowlistProof.maxQuantityInAllowlist == 0 ||
76+
claimCondition.merkleRoot == bytes32(0);
7277

7378
verifyClaim(_dropMsgSender(), _quantity, _currency, _pricePerToken, toVerifyMaxQuantityPerTransaction);
7479

@@ -90,14 +95,16 @@ abstract contract DropSinglePhase is LazyMint, IDrop, IClaimConditionsSinglePhas
9095
// Mint the relevant NFTs to claimer.
9196
uint256 startTokenId = transferTokensOnClaim(_receiver, _quantity);
9297

93-
emit TokensClaimed(0, _dropMsgSender(), _receiver, startTokenId, _quantity);
98+
emit TokensClaimed(_dropMsgSender(), _receiver, startTokenId, _quantity);
9499

95100
_afterClaim(_receiver, _quantity, _currency, _pricePerToken, _allowlistProof, _data);
96101
}
97102

98103
/// @dev Lets a contract admin set claim conditions.
99104
function setClaimConditions(ClaimCondition calldata _condition, bool _resetClaimEligibility) external override {
100-
require(_canSetClaimConditions(), "Not authorized");
105+
if (!_canSetClaimConditions()) {
106+
revert DropSinglePhase__NotAuthorized();
107+
}
101108

102109
bytes32 targetConditionId = conditionId;
103110
uint256 supplyClaimedAlready = claimCondition.supplyClaimed;
@@ -107,7 +114,9 @@ abstract contract DropSinglePhase is LazyMint, IDrop, IClaimConditionsSinglePhas
107114
targetConditionId = keccak256(abi.encodePacked(_dropMsgSender(), block.number));
108115
}
109116

110-
require(supplyClaimedAlready <= _condition.maxClaimableSupply, "max supply claimed already");
117+
if (supplyClaimedAlready > _condition.maxClaimableSupply) {
118+
revert DropSinglePhase__MaxSupplyClaimedAlready(supplyClaimedAlready);
119+
}
111120

112121
claimCondition = ClaimCondition({
113122
startTimestamp: _condition.startTimestamp,
@@ -134,24 +143,42 @@ abstract contract DropSinglePhase is LazyMint, IDrop, IClaimConditionsSinglePhas
134143
) public view {
135144
ClaimCondition memory currentClaimPhase = claimCondition;
136145

137-
require(
138-
_currency == currentClaimPhase.currency && _pricePerToken == currentClaimPhase.pricePerToken,
139-
"invalid currency or price."
140-
);
146+
if (_currency != currentClaimPhase.currency || _pricePerToken != currentClaimPhase.pricePerToken) {
147+
revert DropSinglePhase__InvalidCurrencyOrPrice(
148+
_currency,
149+
currentClaimPhase.currency,
150+
_pricePerToken,
151+
currentClaimPhase.pricePerToken
152+
);
153+
}
141154

142155
// If we're checking for an allowlist quantity restriction, ignore the general quantity restriction.
143-
require(
144-
_quantity > 0 &&
145-
(!verifyMaxQuantityPerTransaction || _quantity <= currentClaimPhase.quantityLimitPerTransaction),
146-
"invalid quantity."
147-
);
148-
require(
149-
currentClaimPhase.supplyClaimed + _quantity <= currentClaimPhase.maxClaimableSupply,
150-
"exceed max claimable supply."
151-
);
156+
if (
157+
_quantity == 0 ||
158+
(verifyMaxQuantityPerTransaction && _quantity > currentClaimPhase.quantityLimitPerTransaction)
159+
) {
160+
revert DropSinglePhase__InvalidQuantity();
161+
}
162+
163+
if (currentClaimPhase.supplyClaimed + _quantity > currentClaimPhase.maxClaimableSupply) {
164+
revert DropSinglePhase__ExceedMaxClaimableSupply(
165+
currentClaimPhase.supplyClaimed,
166+
currentClaimPhase.maxClaimableSupply
167+
);
168+
}
152169

153170
(uint256 lastClaimedAt, uint256 nextValidClaimTimestamp) = getClaimTimestamp(_claimer);
154-
require(lastClaimedAt == 0 || block.timestamp >= nextValidClaimTimestamp, "cannot claim.");
171+
if (
172+
currentClaimPhase.startTimestamp > block.timestamp ||
173+
(lastClaimedAt != 0 && block.timestamp < nextValidClaimTimestamp)
174+
) {
175+
revert DropSinglePhase__CannotClaimYet(
176+
block.timestamp,
177+
currentClaimPhase.startTimestamp,
178+
lastClaimedAt,
179+
nextValidClaimTimestamp
180+
);
181+
}
155182
}
156183

157184
/// @dev Checks whether a claimer meets the claim condition's allowlist criteria.
@@ -168,12 +195,17 @@ abstract contract DropSinglePhase is LazyMint, IDrop, IClaimConditionsSinglePhas
168195
currentClaimPhase.merkleRoot,
169196
keccak256(abi.encodePacked(_claimer, _allowlistProof.maxQuantityInAllowlist))
170197
);
171-
require(validMerkleProof, "not in whitelist.");
172-
require(!usedAllowlistSpot[conditionId].get(merkleProofIndex), "proof claimed.");
173-
require(
174-
_allowlistProof.maxQuantityInAllowlist == 0 || _quantity <= _allowlistProof.maxQuantityInAllowlist,
175-
"invalid quantity proof."
176-
);
198+
if (!validMerkleProof) {
199+
revert DropSinglePhase__NotInWhitelist();
200+
}
201+
202+
if (usedAllowlistSpot[conditionId].get(merkleProofIndex)) {
203+
revert DropSinglePhase__ProofClaimed();
204+
}
205+
206+
if (_allowlistProof.maxQuantityInAllowlist != 0 && _quantity > _allowlistProof.maxQuantityInAllowlist) {
207+
revert DropSinglePhase__InvalidQuantityProof(_allowlistProof.maxQuantityInAllowlist);
208+
}
177209
}
178210
}
179211

Diff for: contracts/feature/LazyMint.sol

+11-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ pragma solidity ^0.8.0;
33

44
import "./interface/ILazyMint.sol";
55

6+
/**
7+
* Thirdweb's `LazyMint` is a contract extension for any base NFT contract. It lets you 'lazy mint' any number of NFTs
8+
* at once. Here, 'lazy mint' means defining the metadata for particular tokenIds of your NFT contract, without actually
9+
* minting a non-zero balance of NFTs of those tokenIds.
10+
*/
11+
612
abstract contract LazyMint is ILazyMint {
713
/// @dev Largest tokenId of each batch of tokens with the same baseURI.
814
uint256[] private batchIds;
@@ -17,7 +23,9 @@ abstract contract LazyMint is ILazyMint {
1723

1824
/// @dev Returns the id for the batch of tokens the given tokenId belongs to.
1925
function getBatchIdAtIndex(uint256 _index) public view returns (uint256) {
20-
require(_index < getBaseURICount(), "invalid index.");
26+
if (_index >= getBaseURICount()) {
27+
revert LazyMint__InvalidIndex(_index);
28+
}
2129
return batchIds[_index];
2230
}
2331

@@ -32,7 +40,7 @@ abstract contract LazyMint is ILazyMint {
3240
}
3341
}
3442

35-
revert("No batch id for token.");
43+
revert LazyMint__NoBatchIDForToken(_tokenId);
3644
}
3745

3846
/// @dev Returns the baseURI for a token. The intended metadata URI for the token is baseURI + tokenId.
@@ -46,7 +54,7 @@ abstract contract LazyMint is ILazyMint {
4654
}
4755
}
4856

49-
revert("No base URI for token.");
57+
revert LazyMint__NoBaseURIForToken(_tokenId);
5058
}
5159

5260
/// @dev Sets the base URI for the batch of tokens with the given batchId.

Diff for: contracts/feature/LazyMintERC721.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import "../lib/TWStrings.sol";
77
abstract contract LazyMintERC721 is LazyMint {
88
using TWStrings for uint256;
99

10-
event TokensLazyMinted(uint256 startTokenId, uint256 endTokenId, string baseURI, bytes extraData);
10+
event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes extraData);
1111

1212
/// @dev the next available non-minted token id
1313
uint256 public nextTokenIdToMint;

Diff for: contracts/feature/Ownable.sol

+9-1
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,21 @@ pragma solidity ^0.8.0;
33

44
import "./interface/IOwnable.sol";
55

6+
/**
7+
* Thirdweb's `Ownable` is a contract extension to be used with any base contract. It exposes functions for setting and reading
8+
* who the 'owner' of the inheriting smart contract is, and lets the inheriting contract perform conditional logic that uses
9+
* information about who the contract's owner is.
10+
*/
11+
612
abstract contract Ownable is IOwnable {
713
/// @dev Owner of the contract (purpose: OpenSea compatibility)
814
address public override owner;
915

1016
/// @dev Lets a contract admin set a new owner for the contract. The new owner must be a contract admin.
1117
function setOwner(address _newOwner) external override {
12-
require(_canSetOwner(), "Not authorized");
18+
if (!_canSetOwner()) {
19+
revert Ownable__NotAuthorized();
20+
}
1321
_setupOwner(_newOwner);
1422
}
1523

Diff for: contracts/feature/Permissions.sol

+8-2
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,15 @@ contract Permissions is IPermissions {
2727
return true;
2828
}
2929

30-
function getRoleAdmin(bytes32 role) public view override returns (bytes32) {
30+
function getRoleAdmin(bytes32 role) external view override returns (bytes32) {
3131
return _getRoleAdmin[role];
3232
}
3333

3434
function grantRole(bytes32 role, address account) public virtual override {
3535
_checkRole(_getRoleAdmin[role], msg.sender);
36+
if (_hasRole[role][account]) {
37+
revert Permissions__CanOnlyGrantToNonHolders(account);
38+
}
3639
_setupRole(role, account);
3740
}
3841

@@ -42,7 +45,9 @@ contract Permissions is IPermissions {
4245
}
4346

4447
function renounceRole(bytes32 role, address account) public virtual override {
45-
require(msg.sender == account, "Can only renounce for self");
48+
if (msg.sender != account) {
49+
revert Permissions__CanOnlyRenounceForSelf(msg.sender, account);
50+
}
4651
_revokeRole(role, account);
4752
}
4853

@@ -58,6 +63,7 @@ contract Permissions is IPermissions {
5863
}
5964

6065
function _revokeRole(bytes32 role, address account) internal virtual {
66+
_checkRole(role, account);
6167
delete _hasRole[role][account];
6268
emit RoleRevoked(role, account, msg.sender);
6369
}

Diff for: contracts/feature/PermissionsEnumerable.sol

+3-12
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ contract PermissionsEnumerable is IPermissionsEnumerable, Permissions {
2121
if (roleMembers[role].members[i] != address(0)) {
2222
if (check == index) {
2323
member = roleMembers[role].members[i];
24+
return member;
2425
}
2526
} else {
2627
check += 1;
@@ -38,18 +39,8 @@ contract PermissionsEnumerable is IPermissionsEnumerable, Permissions {
3839
}
3940
}
4041

41-
function grantRole(bytes32 role, address account) public override(IPermissions, Permissions) {
42-
super.grantRole(role, account);
43-
_addMember(role, account);
44-
}
45-
46-
function revokeRole(bytes32 role, address account) public override(IPermissions, Permissions) {
47-
super.revokeRole(role, account);
48-
_removeMember(role, account);
49-
}
50-
51-
function renounceRole(bytes32 role, address account) public override(IPermissions, Permissions) {
52-
super.renounceRole(role, account);
42+
function _revokeRole(bytes32 role, address account) internal override {
43+
super._revokeRole(role, account);
5344
_removeMember(role, account);
5445
}
5546

Diff for: contracts/feature/PlatformFee.sol

+12-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ pragma solidity ^0.8.0;
33

44
import "./interface/IPlatformFee.sol";
55

6+
/**
7+
* Thirdweb's `PlatformFee` is a contract extension to be used with any base contract. It exposes functions for setting and reading
8+
* the recipient of platform fee and the platform fee basis points, and lets the inheriting contract perform conditional logic
9+
* that uses information about platform fees, if desired.
10+
*/
11+
612
abstract contract PlatformFee is IPlatformFee {
713
/// @dev The address that receives all platform fees from all sales.
814
address private platformFeeRecipient;
@@ -17,13 +23,17 @@ abstract contract PlatformFee is IPlatformFee {
1723

1824
/// @dev Lets a contract admin update the platform fee recipient and bps
1925
function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) external override {
20-
require(_canSetPlatformFeeInfo(), "Not authorized");
26+
if (!_canSetPlatformFeeInfo()) {
27+
revert PlatformFee__NotAuthorized();
28+
}
2129
_setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps);
2230
}
2331

2432
/// @dev Lets a contract admin update the platform fee recipient and bps
2533
function _setupPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) internal {
26-
require(_platformFeeBps <= 10_000, "Exceeds max bps");
34+
if (_platformFeeBps > 10_000) {
35+
revert PlatformFee__ExceedsMaxBps(_platformFeeBps);
36+
}
2737

2838
platformFeeBps = uint16(_platformFeeBps);
2939
platformFeeRecipient = _platformFeeRecipient;

0 commit comments

Comments
 (0)