Skip to content

Commit

Permalink
Merge pull request #34 from PartyDAO/mitigations
Browse files Browse the repository at this point in the history
Mitigations
  • Loading branch information
arr00 authored Aug 13, 2024
2 parents 8d0e283 + d2cda20 commit 0a051e1
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 102 deletions.
12 changes: 10 additions & 2 deletions deployments/84532.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,18 @@
"deployedArgs": "0x00000000000000000000000085065829b9914ea8f456911e0f6d2f9d9a1705ed",
"version": "0.1.5",
"address": "0xCDD6Bd714f3B1a98aa5891965D8eF39c32BFCAf0"
},
{
"deployedArgs": "0x00000000000000000000000085065829b9914ea8f456911e0f6d2f9d9a1705ed00000000000000000000000085065829b9914ea8f456911e0f6d2f9d9a1705ed",
"version": "0.1.6",
"address": "0x80045B318a27CE477E9faa7f34Efcf853dbCdFFC"
}
],
"constructorArgs": ["PartyDAO"]
"constructorArgs": ["PartyDAO", "partydaoMultisig"]
}
},
"constants": { "PartyDAO": "0x85065829b9914ea8f456911e0f6d2f9d9a1705ed" }
"constants": {
"PartyDAO": "0x85065829b9914ea8f456911e0f6d2f9d9a1705ed",
"partydaoMultisig": "0x85065829b9914ea8f456911e0f6d2f9d9a1705ed"
}
}
64 changes: 46 additions & 18 deletions src/MintERC1155.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import { LibString } from "solady/src/utils/LibString.sol";
/// @custom:security-contact [email protected]
contract MintERC1155 is ERC1155Upgradeable, OwnableUpgradeable, ERC2981Upgradeable {
error MintERC1155_Unauthorized();
error MintERC1155_ArityMismatch();
error MintERC1155_TotalPercentChanceNot100();
error MintERC1155_PercentChance0();
error MintERC1155_ExcessEditions();

event ContractURIUpdated();

Expand Down Expand Up @@ -65,6 +65,10 @@ contract MintERC1155 is ERC1155Upgradeable, OwnableUpgradeable, ERC2981Upgradeab
initializer
{
{
if (editions_.length > 25) {
revert MintERC1155_ExcessEditions();
}

uint256 totalPercentChance = 0;
for (uint256 i = 0; i < editions_.length; i++) {
editions.push();
Expand Down Expand Up @@ -97,9 +101,6 @@ contract MintERC1155 is ERC1155Upgradeable, OwnableUpgradeable, ERC2981Upgradeab
if (msg.sender != MINTER) {
revert MintERC1155_Unauthorized();
}
if (ids.length != amounts.length) {
revert MintERC1155_ArityMismatch();
}
_mintBatch(to, ids, amounts, "");
}

Expand All @@ -115,6 +116,18 @@ contract MintERC1155 is ERC1155Upgradeable, OwnableUpgradeable, ERC2981Upgradeab
return allEditions;
}

/**
* @notice Get the percent chance of each edition. Used for filling orders.
* @return An ordered array of the percent chance of each edition.
*/
function getPercentChances() external view returns (uint256[] memory) {
uint256[] memory percentChances = new uint256[](editions.length);
for (uint256 i = 0; i < editions.length; i++) {
percentChances[i] = editions[i].percentChance;
}
return percentChances;
}

function uri(uint256 tokenId) public view override returns (string memory) {
Edition memory edition = editions[tokenId - 1];

Expand Down Expand Up @@ -153,7 +166,9 @@ contract MintERC1155 is ERC1155Upgradeable, OwnableUpgradeable, ERC2981Upgradeab
return json;
}

function supportsInterface(bytes4 interfaceId)
function supportsInterface(
bytes4 interfaceId
)
public
view
virtual
Expand Down Expand Up @@ -199,29 +214,42 @@ contract MintERC1155 is ERC1155Upgradeable, OwnableUpgradeable, ERC2981Upgradeab
* @notice Check if the given address can receive tokens from this contract
* @param to Address to check if receiving tokens is safe
*/
function safeBatchTransferAcceptanceCheckOnMint(address to) external view returns (bool) {
function safeTransferAcceptanceCheckOnMint(address to) external view returns (bool) {
if (to.code.length == 0) return true;

(bool success, bytes memory res) = to.staticcall{ gas: 400_000 }(
abi.encodeCall(IERC1155Receiver.onERC1155Received, (MINTER, address(0), 1, 1, ""))
);
if (success) {
bytes4 response = abi.decode(res, (bytes4));
if (response != IERC1155Receiver.onERC1155Received.selector) {
return false;
}
} else {
return false;
}

uint256[] memory idOrAmountArray = new uint256[](1);
idOrAmountArray[0] = 1;

bytes memory callData = abi.encodeCall(
IERC1155Receiver.onERC1155BatchReceived, (MINTER, address(0), idOrAmountArray, idOrAmountArray, "")
(success, res) = to.staticcall{ gas: 400_000 }(
abi.encodeCall(
IERC1155Receiver.onERC1155BatchReceived, (MINTER, address(0), idOrAmountArray, idOrAmountArray, "")
)
);

if (to.code.length > 0) {
(bool success, bytes memory res) = to.staticcall{ gas: 400_000 }(callData);
if (success) {
bytes4 response = abi.decode(res, (bytes4));
if (response != IERC1155Receiver.onERC1155BatchReceived.selector) {
return false;
}
} else {
if (success) {
bytes4 response = abi.decode(res, (bytes4));
if (response != IERC1155Receiver.onERC1155BatchReceived.selector) {
return false;
}
} else {
return false;
}

return true;
}

function VERSION() external pure returns (string memory) {
return "0.1.5";
return "0.1.6";
}
}
73 changes: 38 additions & 35 deletions src/NFTMint.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ contract NFTMint is Ownable {
error NFTMint_InvalidPerWalletLimit();
error NFTMint_InvalidMaxMints();
error NFTMint_InvalidOwner();
error NFTMint_InvalidFeeRecipient();
error NFTMint_InsufficientGas();
error NFTMint_InsufficientFee();

event MintCreated(MintERC1155 indexed mint, MintArgs args);
event OrderPlaced(
Expand All @@ -38,8 +39,6 @@ contract NFTMint is Ownable {
uint96 feePerMint;
// Address of the owner of the mint
address payable owner;
// Address to receive the fee
address payable feeRecipient;
// Timestamp when the mint expires
uint40 mintExpiration;
// Merkle root for the allowlist
Expand Down Expand Up @@ -72,10 +71,6 @@ contract NFTMint is Ownable {
uint32 perWalletLimit;
// Timestamp when the mint expires
uint40 mintExpiration;
// Address of the owner of the mint
address payable owner;
// Address to receive the fee
address payable feeRecipient;
// Merkle root for the allowlist
bytes32 allowlistMerkleRoot;
// Mapping of addresses to the number of mints they have made
Expand All @@ -98,12 +93,22 @@ contract NFTMint is Ownable {

/// @notice Next order ID to fill in the `orders` array. All orders before this index have been filled.
uint96 public nextOrderIdToFill;
/// @notice Next nonce to use for random number generation
uint96 private _nextNonce;
/// @notice Mapping of mint to mint information
mapping(MintERC1155 => MintInfo) public mints;
/// @notice Array of all orders placed. Filled orders are deleted.
Order[] public orders;

constructor(address owner_) Ownable(owner_) {
/// @notice Address of the mint fee recipient
address payable public immutable FEE_RECIPIENT;

/// @notice Minimum fee per mint (approximately $0.05)
uint96 public constant MIN_FEE_PER_MINT = 0.00002 ether;

constructor(address owner_, address feeRecipient_) Ownable(owner_) {
MINT_NFT_LOGIC = address(new MintERC1155(address(this)));
FEE_RECIPIENT = payable(feeRecipient_);
}

/**
Expand All @@ -123,8 +128,8 @@ contract NFTMint is Ownable {
if (args.owner == address(0)) {
revert NFTMint_InvalidOwner();
}
if (args.feeRecipient == address(0) && args.feePerMint != 0) {
revert NFTMint_InvalidFeeRecipient();
if (args.feePerMint < MIN_FEE_PER_MINT) {
revert NFTMint_InsufficientFee();
}

MintERC1155 newMint = MintERC1155(
Expand All @@ -135,11 +140,9 @@ contract NFTMint is Ownable {
newMint.initialize(args.owner, args.name, args.imageURI, args.description, args.editions, args.royaltyAmountBps);

MintInfo storage mintInfo = mints[newMint];
mintInfo.owner = args.owner;
mintInfo.remainingMints = args.maxMints;
mintInfo.pricePerMint = args.pricePerMint;
mintInfo.feePerMint = args.feePerMint;
mintInfo.feeRecipient = args.feeRecipient;
mintInfo.perWalletLimit = args.perWalletLimit;
mintInfo.allowlistMerkleRoot = args.allowlistMerkleRoot;
mintInfo.mintExpiration = args.mintExpiration;
Expand All @@ -164,17 +167,17 @@ contract NFTMint is Ownable {
external
payable
{
if (amount == 0 || amount > 100) {
revert NFTMint_InvalidAmount();
}

MintInfo storage mintInfo = mints[mint];

if (mintInfo.mintExpiration < block.timestamp) {
revert NFTMint_MintExpired();
}

uint32 modifiedAmount = uint32(Math.min(amount, mintInfo.remainingMints));
if (modifiedAmount == 0 || modifiedAmount > 100) {
revert NFTMint_InvalidAmount();
}

mintInfo.remainingMints -= modifiedAmount;
uint256 totalCost = (mintInfo.pricePerMint + mintInfo.feePerMint) * modifiedAmount;

Expand All @@ -194,7 +197,7 @@ contract NFTMint is Ownable {
}
}

if (!mint.safeBatchTransferAcceptanceCheckOnMint(msg.sender)) {
if (!mint.safeTransferAcceptanceCheckOnMint(msg.sender)) {
revert NFTMint_BuyerNotAcceptingERC1155();
}

Expand All @@ -203,15 +206,12 @@ contract NFTMint is Ownable {
);

{
bool feeSuccess = true;
if (mintInfo.feePerMint > 0) {
(feeSuccess,) =
mintInfo.feeRecipient.call{ value: mintInfo.feePerMint * modifiedAmount, gas: 100_000 }("");
}
(bool feeSuccess,) = FEE_RECIPIENT.call{ value: mintInfo.feePerMint * modifiedAmount, gas: 100_000 }("");

bool mintProceedsSuccess = true;
if (mintInfo.pricePerMint > 0) {
(mintProceedsSuccess,) =
mintInfo.owner.call{ value: mintInfo.pricePerMint * modifiedAmount, gas: 100_000 }("");
mint.owner().call{ value: mintInfo.pricePerMint * modifiedAmount, gas: 100_000 }("");
}
bool refundSuccess = true;
if (msg.value > totalCost) {
Expand All @@ -232,13 +232,13 @@ contract NFTMint is Ownable {
* @param numOrdersToFill The maximum number of orders to fill. Specify 0 to fill all orders.
*/
function fillOrders(uint96 numOrdersToFill) external {
uint256 nonce = 0;
uint256 nonce = _nextNonce;
uint256 nextOrderIdToFill_ = nextOrderIdToFill;
uint256 finalNextOrderToFill =
numOrdersToFill == 0 ? orders.length : Math.min(orders.length, nextOrderIdToFill_ + numOrdersToFill);

while (nextOrderIdToFill_ < finalNextOrderToFill) {
Order storage currentOrder = orders[nextOrderIdToFill_];
Order memory currentOrder = orders[nextOrderIdToFill_];
if (msg.sender != owner() && currentOrder.orderTimestamp + 1 hours > block.timestamp) {
// Only the owner can fill orders that are less than 1 hour old
break;
Expand All @@ -247,21 +247,21 @@ contract NFTMint is Ownable {
// Don't fill orders in the same block to ensure there is randomness
break;
}
MintERC1155.Edition[] memory editions = currentOrder.mint.getAllEditions();
uint256[] memory percentChances = currentOrder.mint.getPercentChances();

uint256[] memory ids = new uint256[](editions.length);
uint256[] memory amounts = new uint256[](editions.length);
uint256[] memory ids = new uint256[](percentChances.length);
uint256[] memory amounts = new uint256[](percentChances.length);

for (uint256 i = 0; i < editions.length; i++) {
for (uint256 i = 0; i < percentChances.length; i++) {
ids[i] = i + 1;
}

for (uint256 i = 0; i < currentOrder.amount; i++) {
uint256 roll = uint256(keccak256(abi.encodePacked(nonce++, blockhash(block.number - 1)))) % 100;

uint256 cumulativeChance = 0;
for (uint256 j = 0; j < editions.length; j++) {
cumulativeChance += editions[j].percentChance;
for (uint256 j = 0; j < percentChances.length; j++) {
cumulativeChance += percentChances[j];
if (roll < cumulativeChance) {
amounts[j]++;
break;
Expand All @@ -272,7 +272,7 @@ contract NFTMint is Ownable {
emit OrderFilled(currentOrder.mint, nextOrderIdToFill_, currentOrder.to, currentOrder.amount, amounts);

uint256 numNonZero = 0;
for (uint256 i = 0; i < editions.length; i++) {
for (uint256 i = 0; i < percentChances.length; i++) {
if (amounts[i] != 0) {
if (numNonZero < i) {
ids[numNonZero] = ids[i];
Expand All @@ -287,16 +287,19 @@ contract NFTMint is Ownable {
mstore(amounts, numNonZero)
}

// If the mint fails with 500_000 gas, the order is still marked as filled.
try currentOrder.mint.mintBatch{ gas: 500_000 }(currentOrder.to, ids, amounts) { } catch { }
delete orders[nextOrderIdToFill_];
nextOrderIdToFill_++;

if (gasleft() * 63 / 64 < 1_000_000) revert NFTMint_InsufficientGas();
// If the mint fails with 1_000_000 gas, the order is still marked as filled.
try currentOrder.mint.mintBatch{ gas: 1_000_000 }(currentOrder.to, ids, amounts) { } catch { }
}

nextOrderIdToFill = uint96(nextOrderIdToFill_);
_nextNonce = uint96(nonce);
}

function VERSION() external pure returns (string memory) {
return "0.1.5";
return "0.1.6";
}
}
Loading

0 comments on commit 0a051e1

Please sign in to comment.