@> function buyPass(uint256 collectionId) external payable {
require(
collectionId == GENERAL_PASS || collectionId == VIP_PASS || collectionId == BACKSTAGE_PASS,
"Invalid pass ID"
);
require(msg.value == passPrice[collectionId], "Incorrect payment amount");
require(passSupply[collectionId] < passMaxSupply[collectionId], "Max supply reached");
_mint(msg.sender, collectionId, 1, "");
++passSupply[collectionId];
uint256 bonus = (collectionId == VIP_PASS) ? 5e18 : (collectionId == BACKSTAGE_PASS) ? 15e18 : 0;
if (bonus > 0) {
BeatToken(beatToken).mint(msg.sender, bonus);
}
emit PassPurchased(msg.sender, collectionId);
}
function test_MEV_attack_On_Buy_Pass() external {
address randomUser = makeAddr('random');
BulkPassBuyer bulkBuyer = new BulkPassBuyer(festivalPass);
hoax(randomUser, 50 ether);
for(uint i = 0 ; i < BACKSTAGE_MAX_SUPPLY ; i ++){
bulkBuyer.buyPass{value: BACKSTAGE_PRICE}();
}
vm.stopPrank();
assertEq(festivalPass.balanceOf(address(bulkBuyer), 3), BACKSTAGE_MAX_SUPPLY);
vm.prank(user1);
vm.expectRevert("Max supply reached");
festivalPass.buyPass{value: BACKSTAGE_PRICE}(3);
vm.prank(user2);
vm.expectRevert("Max supply reached");
festivalPass.buyPass{value: BACKSTAGE_PRICE}(3);
}
contract BulkPassBuyer{
FestivalPass immutable pass;
constructor(FestivalPass _pass){
pass = _pass;
}
function buyPass() external payable {
pass.buyPass{value: msg.value}(3);
}
function onERC1155Received(address, address, uint256, uint256, bytes memory) public pure returns (bytes4) {
return this.onERC1155Received.selector;
}
}
+ //Enforce per-address purchase limits with cooldown
+ mapping(address => mapping(uint256 => uint256)) private lastPurchaseTime;
+ mapping(address => mapping(uint256 => uint256)) private purchaseCount;
+ uint256 constant PURCHASE_COOLDOWN = 1 hours;
+ uint256 constant MAX_PURCHASES_PER_ADDRESS = 1;
function buyPass(uint256 collectionId) external payable {
+ require(
+ purchaseCount[msg.sender][collectionId] < MAX_PURCHASES_PER_ADDRESS,
+ "Purchase limit exceeded"
+ );
+ require(
+ block.timestamp >= lastPurchaseTime[msg.sender][collectionId] + PURCHASE_COOLDOWN,
+ "Cooldown period active"
+ );
// Must be valid pass ID (1 or 2 or 3)
require(
collectionId == GENERAL_PASS || collectionId == VIP_PASS || collectionId == BACKSTAGE_PASS,
"Invalid pass ID"
);
// Check payment and supply
require(msg.value == passPrice[collectionId], "Incorrect payment amount");
require(passSupply[collectionId] < passMaxSupply[collectionId], "Max supply reached");
// Mint 1 pass to buyer
_mint(msg.sender, collectionId, 1, "");
++passSupply[collectionId];
+ lastPurchaseTime[msg.sender][collectionId] = block.timestamp;
+ purchaseCount[msg.sender][collectionId]++;
// VIP gets 5 BEAT welcome bonus BACKSTAGE gets 15 BEAT welcome bonus
uint256 bonus = (collectionId == VIP_PASS) ? 5e18 : (collectionId == BACKSTAGE_PASS) ? 15e18 : 0;
if (bonus > 0) {
// Mint BEAT tokens to buyer
BeatToken(beatToken).mint(msg.sender, bonus);
}
emit PassPurchased(msg.sender, collectionId);
}
+ //Inducing commit-reveal scheme to bypass MEV-attack
+ mapping(address => bytes32) private commitments;
+ mapping(address => uint256) private commitmentTimestamp;
+ uint256 constant REVEAL_DELAY = 10 minutes;
+ function commitToPurchase(bytes32 commitment) external {
+ commitments[msg.sender] = commitment;
+ commitmentTimestamp[msg.sender] = block.timestamp;
+ }
+ function revealAndPurchase(
+ uint256 collectionId,
+ uint256 nonce
+ ) external payable {
+ require(block.timestamp >= commitmentTimestamp[msg.sender] + REVEAL_DELAY,"Reveal period not reached"
+ );
+ require(keccak256(abi.encodePacked(msg.sender, collectionId, nonce)) == commitments[msg.sender],
+ "Invalid reveal"
+ );
. //Rest of buyPass logic
+ delete commitments[msg.sender];
+ }