In the crypto world, you will often hear about how contracts which looked legitimate were the reason behind a big scam. How are hackers able to execute malicious code from a legitimate looking contract?
We will learn one method today 👀
There will be three contracts - Malicious.sol
, Helper.sol
and Good.sol
. User will be able to enter an eligibility list using Good.sol
which will further call Helper.sol
to keep track of all the users which are eligible.
Malicious.sol
will be designed in such a way that eligibility list can be manipulated, lets see how 👀
Note All of these commands should work smoothly . If you are on windows and face Errors Like
Cannot read properties of null (reading 'pickAlgorithm')
Try Clearing the NPM cache usingnpm cache clear --force
.
Start by creating a new project directory.
mkdir malicious-contracts
Let's start setting up Hardhat inside the malicious-contracts
directory.
cd malicious-contracts
npm init --yes
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat
when prompted, choose the Create a Javascript Project
option and follow the steps.
Start by creating a new file inside the contracts
directory called Good.sol
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "./Helper.sol";
contract Good {
Helper helper;
constructor(address _helper) payable {
helper = Helper(_helper);
}
function isUserEligible() public view returns(bool) {
return helper.isUserEligible(msg.sender);
}
function addUserToList() public {
helper.setUserEligible(msg.sender);
}
fallback() external {}
}
After creating Good.sol
, create a new file inside the contracts
directory named Helper.sol
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Helper {
mapping(address => bool) userEligible;
function isUserEligible(address user) public view returns(bool) {
return userEligible[user];
}
function setUserEligible(address user) public {
userEligible[user] = true;
}
fallback() external {}
}
The last contract that we will create inside the contracts
directory is Malicious.sol
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract Malicious {
address owner;
mapping(address => bool) userEligible;
constructor() {
owner = msg.sender;
}
function isUserEligible(address user) public view returns(bool) {
if(user == owner) {
return true;
}
return false;
}
function setUserEligible(address user) public {
userEligible[user] = true;
}
fallback() external {}
}
You will notice that the fact about Malicious.sol
is that it will generate the same ABI as Helper.sol
even though it has different code within it. This is because ABI only contains function definitions for public variables, functions and events. So Malicious.sol
can be typecasted as Helper.sol
.
Now because Malicious.sol
can be typecasted as Helper.sol
, a malicious owner can deploy Good.sol
with the address of Malicious.sol
instead of Helper.sol
and users will believe that he is indeed using Helper.sol
to create the eligibility list.
In our case, the scam will happen as follows. The scammer will first deploy Good.sol
with the address of Malicious.sol
. Then when the user will enter the eligibility list using addUserToList
function which will work fine because the code for this function is same within Helper.sol
and Malicious.sol
.
The true colours will be observed when the user will try to call isUserEligible
with his address because now this function will always return false
because it calls Malicious.sol
's isUserEligible
function which always returns false
except when its the owner itself, which was not supposed to happen.
Lets try to write a test and see if this scam actually works, create a new file inside the test
folder named attack.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Malicious External Contract", function () {
it("Should change the owner of the Good contract", async function () {
// Deploy the Malicious contract
const Malicious = await ethers.getContractFactory("Malicious");
const maliciousContract = await Malicious.deploy();
await maliciousContract.deployed();
console.log("Malicious Contract's Address", maliciousContract.address);
// Deploy the good contract
const Good = await ethers.getContractFactory("Good");
const goodContract = await Good.deploy(maliciousContract.address, {
value: ethers.utils.parseEther("3"),
});
await goodContract.deployed();
console.log("Good Contract's Address:", goodContract.address);
const [_, addr1] = await ethers.getSigners();
// Now lets add an address to the eligibility list
let tx = await goodContract.connect(addr1).addUserToList();
await tx.wait();
// check if the user is eligible
const eligible = await goodContract.connect(addr1).isUserEligible();
expect(eligible).to.equal(false);
});
});
To run this test, open up your terminal pointing to the root of the directory for this level and execute this command:
npx hardhat test
If all your tests passed, this means that the scam was successful and that the user will never be determined eligible.
Make the address of the external contract public and also get your external contract verified so that all users can view the code
Create a new contract, instead of typecasting an address into a contract inside the constructor. So instead of doing Helper(_helper)
where you are typecasting _helper
address into a contract which may or may not be the Helper
contract, create an explicit new helper contract instance using new Helper()
.
Example
contract Good {
Helper public helper;
constructor() {
helper = new Helper();
}
Wow, lots of learning right? 🤯
Be aware of scammers, you might need to double check the code of a new dApp you want to put money in.
Hope you learnt something from this level. If you have any questions or feel stuck or just want to say Hi, hit us up on our Discord. We look forward to seeing you there!