{"id":15177758,"url":"https://github.com/purrproof/ethernaut-solved","last_synced_at":"2026-02-05T07:30:59.604Z","repository":{"id":252516085,"uuid":"840671934","full_name":"PurrProof/ethernaut-solved","owner":"PurrProof","description":"Ethernaut solutions using Hardhat, Ethers@6, Typescript.","archived":false,"fork":false,"pushed_at":"2024-08-28T10:13:36.000Z","size":229,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-23T14:23:21.346Z","etag":null,"topics":["ctf","ctf-writeups","ethereum","ethernaut","ethernaut-ctf","ethernaut-hardhat","security","solidity"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/PurrProof.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-08-10T10:28:39.000Z","updated_at":"2024-08-28T10:13:39.000Z","dependencies_parsed_at":"2024-08-10T11:47:18.733Z","dependency_job_id":"afc1912c-00e4-4972-9b46-f42aa386a759","html_url":"https://github.com/PurrProof/ethernaut-solved","commit_stats":{"total_commits":42,"total_committers":1,"mean_commits":42.0,"dds":0.0,"last_synced_commit":"bdfdfb48d07857b0d1cc92b1cd6a2ec0161add39"},"previous_names":["purrproof/ethernaut-solved"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/PurrProof/ethernaut-solved","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PurrProof%2Fethernaut-solved","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PurrProof%2Fethernaut-solved/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PurrProof%2Fethernaut-solved/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PurrProof%2Fethernaut-solved/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/PurrProof","download_url":"https://codeload.github.com/PurrProof/ethernaut-solved/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PurrProof%2Fethernaut-solved/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266151524,"owners_count":23884436,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ctf","ctf-writeups","ethereum","ethernaut","ethernaut-ctf","ethernaut-hardhat","security","solidity"],"created_at":"2024-09-27T14:43:01.821Z","updated_at":"2026-02-05T07:30:59.594Z","avatar_url":"https://github.com/PurrProof.png","language":"TypeScript","readme":"# Ethernaut game solutions with Hardhat/Typescript/Mocha.js/Ethers\n\n## Quickstart\n\n```shell\ngit clone https://github.com/PurrProof/ethernaut-solved.git\ncd ethernaut-solved\ngit submodule update --init\ncp .env.example .env\npnpm it\n```\n\n## Useful snippets\n\n```\nconst prov = new _ethers.providers.Web3Provider(window.ethereum);\nawait prov.getStorageAt(await contract.address,2)\nsol2uml storage -d -u $RPC_NODE_URL -c Privacy -s $PRIVACY_INSTANCE_ADDRESS -o storage.svg ./Privacy.sol\ncast send -i -r $RPC_NODE_URL --create $BYTECODE\ncast call -i -r $RPC_NODE_URL $ADDRESS $FUNCTION_ID\n```\n\n## Solutions\n\n### 0. Instance. [Level](https://ethernaut.openzeppelin.com/level/0), solution: [test](test/00-instance.ts)\n\n- perform list of view/pure functions calls\n- submit read password to authenticate() function\n\n### 1. Fallback. [Level](https://ethernaut.openzeppelin.com/level/1), solution: [test](test/01-fallback.ts)\n\n- call levelInstance().contribute({value:1})\n- transfer to contract 1 wei, it will trigger receive() function, which will transfer ownership to sender\n- call levelInstance().withdraw() to withdraw all funds\n\n### 2. Fallout. [Level](https://ethernaut.openzeppelin.com/level/2), solution: [test](test/02-fallout.ts)\n\n- to claim ownership, just call instance.Fallout() function (which is not a contstructor)\n\n### 3. CoinFlip. [Level](https://ethernaut.openzeppelin.com/level/3), solution: [contract](contracts/MyCoinFlipAttack.sol), [test](test/03-coinflip.ts)\n\n- repeat coin flip logic in the attacker contract\n- make 10 guesses, one per block\n\n### 4. Telephone. [Level](https://ethernaut.openzeppelin.com/level/4), solution: [contract](contracts/MyTelephoneAttack.sol), [test](test/04-telephone.ts)\n\n- tx.origin != msg.sender: call target contract through proxxy (attacker) contract\n\n### 5. Token. [Level](https://ethernaut.openzeppelin.com/level/5), solution: [test](test/05-token.ts)\n\n**Attack vector**\n\n- underflow in the transfer() function\n\n**How to avoid**\n\n- use solidity 0.8.0+, there is a checked arithmetics by default\n- use libraries like the SafeMath for the older solidity versions\n\n### 6. Delegation. [Level](https://ethernaut.openzeppelin.com/level/6), solution: [test](test/06-delegation.ts)\n\n**Attack vector**\n\nSend `payload=Delegate.pwn.selector` to the `Delegation` contract. The call triggers the `fallback()` function, which\ndelegates execution to the `Delegate` contract, where the owner storage variable is changed to `msg.sender`. Since the\ncall is executed in the context of `Delegation`, its `owner` storage variable, located in the 0th slot, is the one that\ngets changed.\n\n**How to avoid**\n\nSecure the `fallback` function with access control or avoid using `delegatecall` in it. Explicitly define functions to\nprevent unauthorized state changes.\n\n### 7. Force. [Level](https://ethernaut.openzeppelin.com/level/7), solution: [contract](contracts/MyTelephoneAttack.sol), [test](test/04-telephone.ts)\n\n- the EVM doesn't prevent self destructing contract from sending funds to either EOA or to SCA\n\n### 8. Vault. [Level](https://ethernaut.openzeppelin.com/level/8), solution: [test](test/08-vault.ts)\n\n- read password from contract storage (1st slot)\n\n### 9. King. [Level](https://ethernaut.openzeppelin.com/level/9), solution: [contract](contracts/MyForceAttack.sol), [test](test/07-force.ts)\n\n- attacker contract should have no payable receive/fallback functions\n- send prize + 1 value from attacker contract to target contract\n\n### 10. Reentrance. [Level](https://ethernaut.openzeppelin.com/level/10), solution: [contract](contracts/MyReentrancyAttack.sol), [test](test/10-reentrancy.ts)\n\n- in single tx: donate amount, withdraw amount, re-enter target in receive() and withdraw(1), causing underflow of\n  attacker balance in mapping\n- deplete target balance in same(or another) tx by calling target.withdraw(target.balance)\n\n### 11. Elevator. [Level](https://ethernaut.openzeppelin.com/level/11), solution: [contract](contracts/MyElevatorAttack.sol), [test](test/11-elevator.ts)\n\n- key is to make Bulding.isLastFloor(...) function which gives different results depends on input data and target's\n  state\n\n### 12. Privacy. [Level](https://ethernaut.openzeppelin.com/level/12), solution: [test](test/12-privacy.ts)\n\n- code is in the last array item, which is situated at the slot #5. Read this slot contents, take upper 16 bytes, that's\n  the password\n- call `instance.unlock(password)`\n- to vizualize level storage, install `sol2uml`, then run\n  `sol2uml storage -d -u $RPC_NODE_URL -c Privacy -s $PRIVACY_INSTANCE_ADDRESS -o storage.svg ./Privacy.sol`\n\n### 13. GateKeeperOne. [Level](https://ethernaut.openzeppelin.com/level/13), solution: [contract](contracts/MyGateKeeper1Attack.sol), [test](test/13-gatekeeper1.ts)\n\n- gasleft() may change because of compiler version and settings, so bruteforce\n- for code, use 2 lower bytes of tx/origin, and upper 32 bits should not be zero\n\n### 14. GateKeeperTwo. [Level](https://ethernaut.openzeppelin.com/level/14), solution: [contract](contracts/MyGateKeeper2Attack.sol), [test](test/14-gatekeeper2.ts)\n\n- victim.enter(...) function should be called in attacker constructor; this way victim.extcodesize(attacker) will be\n  still zero\n- the idea behind \\_gateKey construction is that val XOR (NOT val) =\u003e all bits set\n\n### 15. Naught Coin. [Level](https://ethernaut.openzeppelin.com/level/15), solution: [test](test/15-naughtcoin.ts)\n\n- it's just as simple as `token.connect(player).approve(other, totalAmount)`, then\n  `token.connect(other).transferFrom(player, other. totalAmount)`\n\n### 16. Preservation. [Level](https://ethernaut.openzeppelin.com/level/16), solution: [contract](contracts/MyPreservationAttack.sol), [test](test/16-preservation.ts)\n\n- call second library, it will overwrite 0th slot in storage with address of fake library\n- call first library, faked by us: it will overwrite slots 0-2 in storage, where slot #2 contains owner address\n\n### 17. Recovery. [Level](https://ethernaut.openzeppelin.com/level/17), solution: [test](test/17-recovery.ts)\n\n- contract addresses are deterministic: `new address = keccak256(creatorAddress, nonce)`, where nonce starts from 0 for\n  EOAs, and from 1 for SCAs, in latter case nonce means number of spawned contracts\n\n### 18. Magic Number. [Level](https://ethernaut.openzeppelin.com/level/18), solution: [1 raw bytecode](test/18-magicnumber.ts), [2 assembly](contracts/MyMagicNumAttack.sol)\n\n```asm\n// init code\nPUSH1 0x0a  // sizecopy, 10 bytes (decimal) is size of runtime code\nPUSH1 0x0c  // offset, 13 bytes (decimal) is size of init code\nPUSH1 0x00  // destOffset, target offset in memory\nCODECOPY    // destOffset, offset, sizecopy =\u003e runtime bytecode into memory\nPUSH1 0x0a  // size, 10 bytes (decimal) is size of runtime code\nPUSH1 0x00  // offset\nRETURN      // offset, size =\u003e halt execution, return data from memory\n\n// run time code\nPUSH1 0x2a  // value, 42 decimal\nPUSH1 0x00  // offset\nMSTORE      // offset, value =\u003e save word (32 bytes) to memory\nPUSH1 0x20  // size\nPUSH1 0x00  // offset\nRETURN      // offset, size =\u003e halt execution, return data (32 bytes here) from memory\n```\n\nProposed level description improvement: [pull-request](https://github.com/OpenZeppelin/ethernaut/pull/750)\n\n### 19. Alien Codex. [Level](https://ethernaut.openzeppelin.com/level/19), solution: [contract](contracts/MyAlienCodexAttack.sol)\n\n- investigate storage structure, using contract ABI and getStorage() function\n- optionally, using contract.record(\\_content), check that codex[] takes slot#1, it should store array length\n- see\n  [0.6.0 breaking changes](https://docs.soliditylang.org/en/v0.8.26/060-breaking-changes.html#explicitness-requirements),\n  `Member-access to length of arrays is now always read-only, even for storage arrays. It is no longer possible to resize storage arrays by assigning a new value to their length.`\n- underflow array length by calling retract()\n- calculate shift to address slot #0, it equals to type(uint256).max - keccak256(1) + 1, where keccak256(1) is address\n  of slot, containing 0th array element\n- fill the slot #0 with new owner address with help of revise(i, owner)\n\n### 20. Denial. [Level](https://ethernaut.openzeppelin.com/level/20), solution: [contract](contracts/MyDenialAttack.sol), [test](test/20-denial.ts)\n\n**Attack vector**\n\n- the \"partner\" contract spend all available to it gas (63/64 of total in parent call) in infinite cycle\n- the rest 1/64 gas is not enough to make .transfer()\n\n**How to avoid**\n\n- limit gas for external calls, like .call{gas:N}(\"\")\n- follow Check-Effects-Iteration pattern\n\n### 21. Shop. [Level](https://ethernaut.openzeppelin.com/level/21), solution: [contract](contracts/MyShopAttack.sol), [test](test/21-shop.ts)\n\n**Attack vector**\n\n- attacker contract fake its responses depending on target contract state\n\n**How to avoid**\n\n- don't trust external/untrusted contracts output\n\n### 21. Dex. [Level](https://ethernaut.openzeppelin.com/level/22), solution: [test](test/22-dex.ts)\n\nCurrent DEX works this way:\n\n| User action     | DexT1 | DexT2 | UserT1 | UserT2 |\n| --------------- | ----- | ----- | ------ | ------ |\n| **Initial**     | 100   | 10    | 10     | 10     |\n| **10 T1 -\u003e T2** | 110   | 90    | 0      | 20     |\n| **20 T2 -\u003e T1** | 86    | 110   | 24     | 0      |\n| **24 T1 -\u003e T2** | 110   | 80    | 0      | 30     |\n| **30 T2 -\u003e T1** | 69    | 110   | 41     | 0      |\n| **41 T1 -\u003e T2** | 110   | 45    | 0      | 65     |\n| **45 T2 -\u003e T1** | 0     | 90    | 110    | 20     |\n\nI'd rather use\n[constant product formula](https://docs.uniswap.org/contracts/v2/concepts/protocol-overview/how-uniswap-works)\n\n### 23. Dex Two. [Level](https://ethernaut.openzeppelin.com/level/23), solution: [contract](contracts/MyDex2Attack.sol), [test](test/23-dex2.ts)\n\n**Attack vector**\n\n- attacker swaps self-managed tokens for tokens, registered in the dex\n\n**How to avoid**\n\n- don't allow to swap not registered / not trusted tokens\n\nP.S. I made _too honest_ fake tokens ;) The original solution is more brutal — their fake tokens only have the\n**balanceOf** and **transferFrom** functions, which return just the necessary minimum.\n\n### 24. Puzzle Wallet. [Level](https://ethernaut.openzeppelin.com/level/24), solution: [contract](contracts/MyPuzzleWalletAttack.sol), [test](test/24-puzzlewallet.ts)\n\n1. **Attack Vector**\n\n   - **Storage Collision Exploit**  \n     Exploits a storage collision between the proxy's `pendingAdmin` and the implementation's `owner` to gain control.\n   - **Recursive Multicall Exploit**  \n     Uses `multicall` with a reentrant-like call to drain funds by calling `deposit` twice in one transaction.\n   - **Admin Privilege Hijack**  \n     Overwrites the proxy's `admin` by setting the `maxBalance`, due to storage collision, to take control.\n\n2. **How to Avoid**\n   - **Proper Storage Layout**  \n     Use reserved storage slots to avoid collisions between proxy and implementation contracts.\n   - **Secure Delegatecalls**  \n     Only delegatecall to trusted and verified implementations with compatible storage.\n   - **Restrict Function Combinations**  \n     Limit `multicall` or prevent repeated calls to functions like `deposit` within the same transaction.\n\n### 25. Motorbike. [Level](https://ethernaut.openzeppelin.com/level/25), solution: [contract](contracts/MyMotorbikeAttack.sol), [test](test/25-motorbike.ts)\n\n**Attack vector**\n\n- Take upgrader role of implementation contract (Engine). It's possible because initialize() function is not disabled\n  and opened to everyone.\n- Change Engine's implementation to attacker contract, then call attacker's method, which contains selfdestruct()\n\n**How to avoid**\n\n- disable initializer in Engine contract\n\nP.S. [discussion](https://github.com/OpenZeppelin/ethernaut/issues/701)\n\n### 26. Double Entry Point. [Level](https://ethernaut.openzeppelin.com/level/26), solution: [detection bot](contracts/MyDoubleEntryDetectionBot.sol), [test](test/26-doubleentry.ts)\n\n### 27. Good Samaritan. [Level](https://ethernaut.openzeppelin.com/level/27), solution: [contract](contracts/MyGoodSamaritanAttack.sol), [test](test/27-goodsamaritan.ts)\n\n**Attack vector**\n\n- revert with NotEnoughBalance() error in the attacker's contract notify() function\n- error will be bubbled up to GoodSamaritan contract, where rest of balance will be transfered to attacker contract\n- don't revert, if transfer exceeds 10 coins\n\n**How to avoid**\n\n- assume, that errors may be bubbled up by any contract down in the chain\n\n### 28. Gate Keeper Three. [Level](https://ethernaut.openzeppelin.com/level/28), solution: [contract](contracts/MyGateKeeperThreeAttack.sol), [test](test/28-gatekeeper3.ts)\n\n**Attack vector**\n\n- use block.timestamp as password; it's same for all internal transactions within transaction\n\n### 29. Switch. [Level](https://ethernaut.openzeppelin.com/level/29), solution: [contract](contracts/MySwitchAttack.sol), [test](test/29-switch.ts)\n\n**Attack vector**\n\nManually created calldata with non-standard variable offset.\n\n```text\nNormal Call: Switch.flipSwitch(abi.encodeWithSignature(\"turnSwitchOn()\"))\nCalldata:\n    0x30c13ade: selector of flipSwitch(bytes)\n    0x0000000000000000000000000000000000000000000000000000000000000020: offset to the `bytes` parameter area\n    0x0000000000000000000000000000000000000000000000000000000000000004: length of the `bytes` parameter, 4 in this case\n    0x76227e1200000000000000000000000000000000000000000000000000000000: data itself, right-padded\nThat's why offset 68 is hardcoded in the expression `calldatacopy(selector, 68, 4) // grab function selector from calldata`\nI.e. 4 (selector) + 32 (offset) + 32 (length) = 68\n\nSo we need to have selector of the allowed function, i.e. Switch.turnSwitchOff.selector, at the offset 68\nWe'll construct calldata manually:\n    0x30c13ade: selector of flipSwitch(bytes)\n    0x0000000000000000000000000000000000000000000000000000000000000060: offset to the `bytes` parameter area\n    0x0000000000000000000000000000000000000000000000000000000000000000: not used word, it can be anything\n    0x20606e1500000000000000000000000000000000000000000000000000000000: hardcoded Switch.turnSwitchOff.selector\n    0x0000000000000000000000000000000000000000000000000000000000000004: length of flipSwitch's bytes memory _data parameter\n    0x76227e12: data itself, Switch.turnSwitchOn.selector\n```\n\n**How to avoid**\n\n- don't hardcode variable offsets when dealing with calldata at low level\n\n### 30. Higher Order. [Level](https://ethernaut.openzeppelin.com/level/30), solution: [contract](contracts/MyHigherOrderAttack.sol), [test](test/30-higherorder.ts)\n\nhttps://docs.soliditylang.org/en/latest/security-considerations.html#minor-details\n\n\u003e Types that do not occupy the full 32 bytes might contain “dirty higher order bits”. This is especially important if\n\u003e you access msg.data - it poses a malleability risk: You can craft transactions that call a function f(uint8 x) with a\n\u003e raw byte argument of 0xff000001 and with 0x00000001. Both are fed to the contract and both will look like the number 1\n\u003e as far as x is concerned, but msg.data will be different, so if you use keccak256(msg.data) for anything, you will get\n\u003e different results.\n\nhttps://github.com/ethereum/solidity/issues/14766 (closed)\n\n\u003e This no longer seems to be true in Solidity \u003e= 0.8. ABI decoding now reverts when it encounters dirty high-order bits.\n\u003e Can you please confirm this is no longer an issue and add a comment to the documentation (or remove this section), or\n\u003e clarify why this is still an issue in Solidity \u003e= 0.8?\n\n### 31. Stake [Level](https://ethernaut.openzeppelin.com/level/31), solution: [test](test/31-stake.ts)\n\n**Attack Vector**\n\nExploit a bug in `Stake.StakeWETH()`: the return value of the low-level `WETH.transfer()` call isn't checked, though it\ncan return `false`.\n\n**How to Avoid**\n\n- Always check return values of external `ERC-20` token calls.\n- Use OpenZeppelin's `SafeERC20` wrappers for safer `ERC20` function calls.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpurrproof%2Fethernaut-solved","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpurrproof%2Fethernaut-solved","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpurrproof%2Fethernaut-solved/lists"}