Beginner FriendlyFoundryNFT
100 EXP
View results
Submission Details
Severity: medium
Invalid

The `ChoosingRam` contract can be changed at any time which breaks the function call `ChoosingRam::increaseValuesOfParticipants`.

Summary
The ChoosingRam contract can be changed at any time which breaks the function call ChoosingRam::increaseValuesOfParticipants. The ability to manipulate the contract at any point may also lead to reduced trust in the protocol, potentially lower participation rates and subsequently the funds generated.

Description

The function RamNFT::setChoosingRamContract allows the organiser to change the ChoosingRam contract in the RamNFT contract. This enables a malicious organiser to manipulate the access controls of the protocol. Breaking the functionality of the call to ChoosingRam::increaseValuesOfParticipants prevents the players from becoming Ram via this game mechanic and leaves the ability to become Ram solely up to the organiser via the ChoosingRam::selectRamIfNotSelected function call. Changing the contract in this way may break the functionality accidentally or it may be of malicious intent.

The documentation states:

ChoosingRam.sol
This contract allows users to increase their values and select Ram if all characteristics are true. If the user has not selected Ram before 12 October 2024, > then the Organizer can select Ram if not selected.

  • increaseValuesOfParticipants allows users to increase their values(or characteristics) and become Ram for the event. The values will never be updated > again after 12 October 2024.

  • selectRamIfNotSelected - Allows the organizer to select Ram if not selected by the user.

The Relevant code is shown below:

modifier onlyOrganiser() {
if (msg.sender != organiser) {
revert RamNFT__NotOrganiser();
}
_;
}
function setChoosingRamContract(address _choosingRamContract)
public onlyOrganiser {
choosingRamContract = _choosingRamContract;
}

Impact

Trust cannot be established by reviewing the protocol as its internal access control mechanisms can change at any time. This may adversely affect the number of willing participants and reduce the potential number of players who will enter. Changing the contract will break the ability for players to become Ram via the intended mechanism within the players control, that is, the functionality provided by: ChoosingRam::increaseValuesOfParticipants.

Proof of Concept

Standalone Test

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {Dussehra} from "../src/Dussehra.sol";
import {ChoosingRam} from "../src/ChoosingRam.sol";
import { mock } from "../src/mocks/mock.sol";
import {RamNFT} from "../src/RamNFT.sol";
contract ChangeContractTest is Test {
// This is the error we'll get when we change the
// contract and try to call
// choosingRam.increaseValuesOfParticipants
error RamNFT__NotChoosingRamContract();
// Stanard testing harness stuff below
Dussehra public dussehra;
RamNFT public ramNFT;
ChoosingRam public choosingRam;
address public organiser = makeAddr("organiser");
address public player1 = makeAddr("player1");
address public player2 = makeAddr("player2");
address public player3 = makeAddr("player3");
address public player4 = makeAddr("player4");
address public player5 = makeAddr("player5");
modifier participants5() {
vm.startPrank(player1);
vm.deal(player1, 1 ether);
dussehra.enterPeopleWhoLikeRam{value: 1 ether}();
vm.stopPrank();
vm.startPrank(player2);
vm.deal(player2, 1 ether);
dussehra.enterPeopleWhoLikeRam{value: 1 ether}();
vm.stopPrank();
vm.startPrank(player3);
vm.deal(player3, 1 ether);
dussehra.enterPeopleWhoLikeRam{value: 1 ether}();
vm.stopPrank();
vm.startPrank(player4);
vm.deal(player4, 1 ether);
dussehra.enterPeopleWhoLikeRam{value: 1 ether}();
vm.stopPrank();
vm.startPrank(player5);
vm.deal(player5, 1 ether);
dussehra.enterPeopleWhoLikeRam{value: 1 ether}();
vm.stopPrank();
_;
}
function setUp() public {
vm.startPrank(organiser);
ramNFT = new RamNFT();
choosingRam = new ChoosingRam(address(ramNFT));
dussehra = new Dussehra(1 ether, address(choosingRam), address(ramNFT));
ramNFT.setChoosingRamContract(address(choosingRam));
vm.stopPrank();
}
function test_changeContract() public participants5 {
//first ensure we can update the state as expected
vm.startPrank(player1);
choosingRam.increaseValuesOfParticipants(0, 1);
vm.stopPrank();
// now switch out the contract - this will break the ability for
// users to chooseingRam.increaseValuesOfParticipants as when they call
// the function the msg.sender won't match the currently set
// contract which is required by the modifier
// on the RamNFT::updateCharacteristics function call.
//
// Here we are just using address(0) but it could be the address
// of another contract it does not matter though as it
// will fail on the modifier check.
vm.startPrank(organiser);
ramNFT.setChoosingRamContract( address(0) );
vm.stopPrank();
// Same call as before but we expect this to fail now
vm.expectRevert(
abi.encodeWithSelector(RamNFT__NotChoosingRamContract.selector));
vm.startPrank(player1);
choosingRam.increaseValuesOfParticipants(0, 1);
vm.stopPrank();
}
}

Note the: Revert and Error: RamNFT__NotChoosingRamContract() at the bottom of the stack trace in the test results shown below.

forge test --match-path test/ChangeContractTest.t.sol -vvvv
[⠊] Compiling...
[⠆] Compiling 1 files with 0.8.20
[⠰] Solc 0.8.20 finished in 4.23s
Compiler run successful!
Ran 1 test for test/ChangeContractTest.t.sol:ChangeContractTest
[PASS] test_changeContract() (gas: 734774)
Traces:
[734774] ChangeContractTest::test_changeContract()
├─ [0] VM::startPrank(player1: [0x7026B763CBE7d4E72049EA67E89326432a50ef84])
│ └─ ← [Return]
├─ [0] VM::deal(player1: [0x7026B763CBE7d4E72049EA67E89326432a50ef84], 1000000000000000000 [1e18])
│ └─ ← [Return]
├─ [167375] Dussehra::enterPeopleWhoLikeRam{value: 1000000000000000000}()
│ ├─ [92338] RamNFT::mintRamNFT(player1: [0x7026B763CBE7d4E72049EA67E89326432a50ef84])
│ │ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: player1: [0x7026B763CBE7d4E72049EA67E89326432a50ef84], tokenId: 0)
│ │ └─ ← [Stop]
│ ├─ emit PeopleWhoLikeRamIsEntered(competitor: player1: [0x7026B763CBE7d4E72049EA67E89326432a50ef84])
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::startPrank(player2: [0xEb0A3b7B96C1883858292F0039161abD287E3324])
│ └─ ← [Return]
├─ [0] VM::deal(player2: [0xEb0A3b7B96C1883858292F0039161abD287E3324], 1000000000000000000 [1e18])
│ └─ ← [Return]
├─ [117075] Dussehra::enterPeopleWhoLikeRam{value: 1000000000000000000}()
│ ├─ [70438] RamNFT::mintRamNFT(player2: [0xEb0A3b7B96C1883858292F0039161abD287E3324])
│ │ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: player2: [0xEb0A3b7B96C1883858292F0039161abD287E3324], tokenId: 1)
│ │ └─ ← [Stop]
│ ├─ emit PeopleWhoLikeRamIsEntered(competitor: player2: [0xEb0A3b7B96C1883858292F0039161abD287E3324])
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::startPrank(player3: [0xcC37919fDb8E2949328cDB49E8bAcCb870d0c9f3])
│ └─ ← [Return]
├─ [0] VM::deal(player3: [0xcC37919fDb8E2949328cDB49E8bAcCb870d0c9f3], 1000000000000000000 [1e18])
│ └─ ← [Return]
├─ [117075] Dussehra::enterPeopleWhoLikeRam{value: 1000000000000000000}()
│ ├─ [70438] RamNFT::mintRamNFT(player3: [0xcC37919fDb8E2949328cDB49E8bAcCb870d0c9f3])
│ │ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: player3: [0xcC37919fDb8E2949328cDB49E8bAcCb870d0c9f3], tokenId: 2)
│ │ └─ ← [Stop]
│ ├─ emit PeopleWhoLikeRamIsEntered(competitor: player3: [0xcC37919fDb8E2949328cDB49E8bAcCb870d0c9f3])
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::startPrank(player4: [0x4AB770c39b683c352367fC63C468F18b201d4bd0])
│ └─ ← [Return]
├─ [0] VM::deal(player4: [0x4AB770c39b683c352367fC63C468F18b201d4bd0], 1000000000000000000 [1e18])
│ └─ ← [Return]
├─ [117075] Dussehra::enterPeopleWhoLikeRam{value: 1000000000000000000}()
│ ├─ [70438] RamNFT::mintRamNFT(player4: [0x4AB770c39b683c352367fC63C468F18b201d4bd0])
│ │ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: player4: [0x4AB770c39b683c352367fC63C468F18b201d4bd0], tokenId: 3)
│ │ └─ ← [Stop]
│ ├─ emit PeopleWhoLikeRamIsEntered(competitor: player4: [0x4AB770c39b683c352367fC63C468F18b201d4bd0])
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::startPrank(player5: [0x6f02224B51d99B1BaED8BBCED2edFC03BeC6FaFF])
│ └─ ← [Return]
├─ [0] VM::deal(player5: [0x6f02224B51d99B1BaED8BBCED2edFC03BeC6FaFF], 1000000000000000000 [1e18])
│ └─ ← [Return]
├─ [117075] Dussehra::enterPeopleWhoLikeRam{value: 1000000000000000000}()
│ ├─ [70438] RamNFT::mintRamNFT(player5: [0x6f02224B51d99B1BaED8BBCED2edFC03BeC6FaFF])
│ │ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: player5: [0x6f02224B51d99B1BaED8BBCED2edFC03BeC6FaFF], tokenId: 4)
│ │ └─ ← [Stop]
│ ├─ emit PeopleWhoLikeRamIsEntered(competitor: player5: [0x6f02224B51d99B1BaED8BBCED2edFC03BeC6FaFF])
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::startPrank(player1: [0x7026B763CBE7d4E72049EA67E89326432a50ef84])
│ └─ ← [Return]
├─ [13524] ChoosingRam::increaseValuesOfParticipants(0, 1)
│ ├─ [405] RamNFT::tokenCounter() [staticcall]
│ │ └─ ← [Return] 5
│ ├─ [405] RamNFT::tokenCounter() [staticcall]
│ │ └─ ← [Return] 5
│ ├─ [1135] RamNFT::getCharacteristics(0) [staticcall]
│ │ └─ ← [Return] CharacteristicsOfRam({ ram: 0x7026B763CBE7d4E72049EA67E89326432a50ef84, isJitaKrodhah: false, isDhyutimaan: false, isVidvaan: false, isAatmavan: false, isSatyavaakyah: false })
│ ├─ [1135] RamNFT::getCharacteristics(1) [staticcall]
│ │ └─ ← [Return] CharacteristicsOfRam({ ram: 0xEb0A3b7B96C1883858292F0039161abD287E3324, isJitaKrodhah: false, isDhyutimaan: false, isVidvaan: false, isAatmavan: false, isSatyavaakyah: false })
│ ├─ [3680] RamNFT::updateCharacteristics(1, true, false, false, false, false)
│ │ └─ ← [Stop]
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::startPrank(organiser: [0xe81f335f0c35d66819c4dF203d728f579880b4b1])
│ └─ ← [Return]
├─ [5600] RamNFT::setChoosingRamContract(0x0000000000000000000000000000000000000000)
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::expectRevert(RamNFT__NotChoosingRamContract())
│ └─ ← [Return]
├─ [0] VM::startPrank(player1: [0x7026B763CBE7d4E72049EA67E89326432a50ef84])
│ └─ ← [Return]
├─ [10993] ChoosingRam::increaseValuesOfParticipants(0, 1)
│ ├─ [405] RamNFT::tokenCounter() [staticcall]
│ │ └─ ← [Return] 5
│ ├─ [405] RamNFT::tokenCounter() [staticcall]
│ │ └─ ← [Return] 5
│ ├─ [1135] RamNFT::getCharacteristics(0) [staticcall]
│ │ └─ ← [Return] CharacteristicsOfRam({ ram: 0x7026B763CBE7d4E72049EA67E89326432a50ef84, isJitaKrodhah: false, isDhyutimaan: false, isVidvaan: false, isAatmavan: false, isSatyavaakyah: false })
│ ├─ [1135] RamNFT::getCharacteristics(1) [staticcall]
│ │ └─ ← [Return] CharacteristicsOfRam({ ram: 0xEb0A3b7B96C1883858292F0039161abD287E3324, isJitaKrodhah: true, isDhyutimaan: false, isVidvaan: false, isAatmavan: false, isSatyavaakyah: false })
│ ├─ [1135] RamNFT::getCharacteristics(1) [staticcall]
│ │ └─ ← [Return] CharacteristicsOfRam({ ram: 0xEb0A3b7B96C1883858292F0039161abD287E3324, isJitaKrodhah: true, isDhyutimaan: false, isVidvaan: false, isAatmavan: false, isSatyavaakyah: false })
│ ├─ [887] RamNFT::updateCharacteristics(1, true, true, false, false, false)
│ │ └─ ← [Revert] RamNFT__NotChoosingRamContract()
│ └─ ← [Revert] RamNFT__NotChoosingRamContract()
├─ [0] VM::stopPrank()
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.26ms (1.13ms CPU time)
Ran 1 test suite in 482.04ms (4.26ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended mitigation

  • Remove the ability to change the ChoosingRam contract after it has been set.

References
https://github.com/Cyfrin/2024-06-Dussehra/blob/9c86e1b09ed9516bfbb3851c145929806da75d87/src/RamNFT.sol#L45

Tools Used

  • Manual Review

Updates

Lead Judging Commences

bube Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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