The Hacking of Rubixi Smart Contract

Aitor Zaldua
Block Magnates
Published in
4 min readOct 3, 2022

This is a True Story.”

When the movie starts with that sentence, something bad is going to happen to our protagonist. Maybe he’ll die, maybe he’ll lose his love forever… or maybe someone will take control of his smart contract and steal his funds because of a silly mistake.

One REALLY silly mistake.

This is the story of a company called Rubixy and how it tried to be a Ponzi scheme, and fool a lot of people, and ended up being the laughing stock of the smart contracts ecosystem.

In this series of Open Zeppelin Ethernaut Challenges, we continue with Ethernaut 2, The Fal1out. The challenge, in this case, is:

Claim ownership of the contract below to complete this level.

Great, we already done this before.

First of all, as auditors, we have to understand the contract:

The constructor:

contract Fallout {

using SafeMath for uint256;
mapping (address => uint) allocations;
address payable public owner;
/* constructor */
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}

Ok, as always, the contract starts by assigning the property of the contract to the deployer. This is an old way of calling the constructor, we will rewrite it later, in this case, when a function has the same name as the contract, Fallout, this is the constructor.

¿Do you notice something wrong here? Yes! you have the solution! Great little padawan, but let´s continue with the auditing process.

The functions:

function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(msg.value);
}
function sendAllocation(address payable allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}
function collectAllocations() public onlyOwner {
msg.sender.transfer(address(this).balance);
}
function allocatorBalance(address allocator) public view returns (uint) {
return allocations[allocator];
}

Oki doki, this is a bunch of functions to send ether to the contract. Look at collectAllocations(), it is an OnlyOwner function and is the only one that can drain the contract.

So, if we are the owner, we will be able to transfer all ethers to our account. Forever.

And, remember, this is a true history.

The second step of an auditor (or a black hat hacker, hahahaha!!!! ⇒ don’t do that, please be a good person) is to execute functions or parameters to continue with the understanding of the contract.

For example, collect the player address (our address) and the owner’s address. We did it before in the previous challenge.

Wait, wait, what is going on here? The contract has no owner?

Yes, that’s right. In the past, the Rubixi company was originally called Dynamic Piramid and when they decided to change the name to Rubixi, they changed the name of the contract but forgot to do the same with the constructor.

In our example the name of the contract is Fallout and the name of the constructor is Fal1out so the Fal1out function is not marked as a constructor and can be called by anyone.

And now,

Boomshakalaka! we are the owner.

Now, we can run to the victory and press the submit button.

I am going to stop here to write a love letter to my dear developers:

“Dear Dev,

Do you know why smart contracts are so special? Because they are immutable. IN-MU-TA-BLES. You will die, your city will become a mass of scrap metal and humanity will be destroyed by an alien invasion.

But your smart contracts will be still there. Inmutables. They don´t care about you. If you make a mistake, they will remind you forever.

So give them love. Make them look pretty. The eternity will appreciate.

KR, Aitor”

For example, here is the original contract of Rubixi, still on the Ethereum mainnet:

address: 0xe82719202e5965Cf5D9B6673B7503a3b92DE20be

Remember that you need eth to interact with it, to pay the gas, so do it at your own risk (I didn’t and won’t).

And for auditors, never take anything for granted. Always be methodical.

Finally, let´s rewrite the code following the 2022 standards:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@openzeppelin/contracts/access/Ownable.sol";contract Fallout is Ownable { mapping (address => uint) allocations; constructor() payable {
allocations[msg.sender] = msg.value;
}
}
  • The safeMath contract is no longer required.
  • The Open Zeppelin Ownable contract directly assigns ownership to the deployer.
  • We use the keyword constructor, for the constructor, so we will not make the same mistake again (The Rubixi contract was built with v0.4.24)

I hope you enjoyed this article and feel free to contact me here or on twitter, @azdraft_.

Happy Hacking!

--

--

Written by Aitor Zaldua

Security Researcher | Smart Contract Dev | Blockchain Instructor. Follow me on twitter: @azdraft_

No responses yet