Long, long ago (like... Block 42), a wizard has sealed 1 ETH inside a mystical Proxy Contract. You get one shot to proxy upgrade it—but under these very strict rules:
No Messing with the Family Tree The inheritance structure stays exactly as is. No new parents, no secret children.
No Rewriting the Magic You can’t alter existing functions or their visibility, and you can’t add or remove any functions. No new spells, no banished spells.
No Rearranging the Royal Closet. The storage layout cannot change. Touch a single uint256, and you might awaken the alignment demon.
No Upgrading the Wizard’s Quill Keep the same Solidity version. The wizard likes his dusty old version—deal with it.
Obey these ancient laws, upgrade the contract once, and claim the 1 ETH prize. But break them and face the dreaded 'Gasless Abyss!'
fromtypingimportDictfromweb3importWeb3frombase64importb64decodeimportrequestsimportsecretsfrometh_abiimportabifromctf_launchers.launcherimportAction,pprintfromctf_launchers.pwn_launcherimportPwnChallengeLauncherfromctf_launchers.utilsimportdeployfromctf_server.typesimportLaunchAnvilInstanceArgs,UserData,get_privileged_web3,get_system_accountfromfoundry.anvilimportcheck_errorfromfoundry.anvilimportanvil_autoImpersonateAccount,anvil_setCodeclassChallenge(PwnChallengeLauncher):defafter_init(self):self._actions.append(Action(name="Upgrade the CTF contract",handler=self.upgrade_contract))defget_anvil_instances(self)->Dict[str,LaunchAnvilInstanceArgs]:return{"main":self.get_anvil_instance(fork_url=None,balance=1)}defupgrade_contract(self):user_data=self.get_user_data()pprint('Please input the new full source code in Base64.')pprint('Terminal has a 1024 character limit on copy paste, so you can paste it in batches and finish with an empty one.')total_txt=''next_txt='1337'whilenext_txt!='':next_txt=input('Input:\n')total_txt+=next_txttry:upgrade_contract=b64decode(total_txt).decode()exceptExceptionase:returnwithopen('challenge/project/src/CTF.sol','r')asf:original_contract=f.read()try:res=requests.post('http://restricted-proxy-backend:3000/api/compare',json={'originalContract':original_contract,'upgradeContract':upgrade_contract}).json()exceptExceptionase:returnif'error'inresornotres['areEqual']:pprint('Nope, sorry, that contract violates the upgrade rules.')returnweb3=get_privileged_web3(user_data,"main")(ctf_addr,)=abi.decode(["address"],web3.eth.call({"to":user_data['metadata']["challenge_address"],"data":web3.keccak(text="ctf()")[:4],}),)anvil_setCode(web3,ctf_addr,res['bytecode'])pprint('All okay! The CTF contract has been upgraded.')Challenge().run()
The contract CTF has 100 ether. The function withdrawFunds can be called once by the owner, and the withdraw amount is related to the withdrawRate.
The first parameter of the function becomeOwner is of type uint256, and the function uses calldataload(4) to read the data, so we can just convert the address to a number and set the owner to ourselves.
If we want to withdraw all ETH in the contract, we have to set the withdrawRate to 10000. Although the function changeWithdrawRate also uses calldataload(4) to read the data, its first parameter is of type uint8.
Since Solidity v0.8.0, ABI coder v2 is activated by default. It performs more sanity checks on the inputs than v1. Due to the limitation of the parameter type, we can not set withdrawRate to 10000.
This challenge supports upgrading the code of the contract CTF, but it will determine whether it follows the rules based on the source code. We can not change the ABI, storage layout, etc.
However, we can choose to use ABI coder v1 by adding pragma abicoder v1;. Therefore, the sanity check on the input is removed, then we can easily update the withdrawRate to 10000.