Root + Impact
Description
Users purchase passes (GENERAL_PASS, VIP_PASS, BACKSTAGE_PASS) via buyPass, receiving ERC1155 tokens and BeatToken bonuses (5e18 for VIP, 15e18 for BACKSTAGE). This Passes are transferable by default, allowing users to buy a pass, claim the bonus, transfer the pass, and repeat, accumulating bonuses.
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);
}
Risk
Likelihood:
Any user with ETH can exploit via safeTransferFrom. No restrictions on bonus claims.
Straightforward attack path (buy, claim, transfer, repeat).
Impact:
Direct fund impact via uncapped BeatToken minting, inflating supply and devaluing rewards.
Severe disruption of tokenomics and secondary market control.
Proof of Concept
function test_BonusAbuseViaTransfer() public {
vm.prank(user1);
festivalPass.buyPass{value: BACKSTAGE_PRICE}(3);
assertEq(beatToken.balanceOf(user1), 15e18);
assertEq(festivalPass.balanceOf(user1, 3), 1);
vm.prank(user1);
festivalPass.safeTransferFrom(user1, user2, 3, 1, "");
assertEq(festivalPass.balanceOf(user1, 3), 0);
assertEq(festivalPass.balanceOf(user2, 3), 1);
vm.prank(user2);
festivalPass.safeTransferFrom(user2, user1, 3, 1, "");
assertEq(festivalPass.balanceOf(user1, 3), 1);
vm.prank(user1);
festivalPass.buyPass{value: BACKSTAGE_PRICE}(3);
assertEq(beatToken.balanceOf(user1), 30e18);
assertEq(festivalPass.balanceOf(user1, 3), 2);
}
Recommended Mitigation
// FestivalPass.sol
+ import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155.sol";
contract FestivalPass is ERC1155, Ownable2Step, IFestivalPass {
+ function _beforeTokenTransfer(
+ address operator,
+ address from,
+ address to,
+ uint256[] memory ids,
+ uint256[] memory amounts,
+ bytes memory data
+ ) internal virtual override {
+ super._beforeTokenTransfer(operator, from, to, ids, amounts, data);
+ for (uint256 i = 0; i < ids.length; i++) {
+ if (ids[i] <= BACKSTAGE_PASS && from != address(0) && to != address(0)) {
+ revert("Passes are non-transferable");
+ }
+ }
+ }
}