My Coding Journey: From Finance to Smart Contract Auditor

I've been really inspired by some of the journeys some users have posted (in particular JulianPerassi and Jshanks21) and the overwhelming support this community offers is something I want to be involved in. Re-iterating a common theme, I want to track my progress through a series of posts to hold myself accountable and create meaningful discussions (hopefully) around relevant security-related topics.

By way of introduction, I work in front-end finance and have had a longstanding interest in how Tradfi has integrated technology into their platforms. My interest in security auditing has evolved from witnessing an increased interest from Tradfi in the benefits Defi but the hesitancy to execute on ideas because of security vulnerabilities. I also find the technical details of an audit to be super interesting and a completely different way to problem solve!

To better assist organisations with resolving their security vulnerabilities, I have signed up for the Metana bootcamp and commenced studying concepts often related to security vulnerabilities (e.g. reentrancy attacks, flash loans, access control issues etc.) .

Being relatively new to Solidity and Web3, I welcome all feedback (don't spare my feelings, I want to always strive to do better) and any tips or chances to collaborate. I also want to make this series as accessible as possible so if something isn't clear let me know.

Without further ado, let's get to it!

5 Likes

Day 1: I've had an on/off relationship with Python since 2020, but after starting my job in finance I realised the value this language has had in structuring my problem-solving logic and scripting repetitive tasks. I started immediately diving into the OpenZepplin smart contract wizard to better understand the basic features of a smart contract.

The first ERC20 contract I created contained the following features (my logic being I can see and replicated various security vulnerabilities), a special address can:

  • steal other people’s funds;
  • create tokens; and
  • destroy tokens.

Each of these were effectively achieved through the following functions:

  • mint(address recipient);
  • changeBalanceAtAddress(address target); and
  • authoritativeTransferFrom(address from, address to).

A screenshot is contained below:

My next post is to further explain some measures that can be included to mitigate the risk of this type of risk (i.e. by introducing a transaction mechanism and potentially sanctions from blacklisted accounts).

The end goal being to establish enough competence to start various challenges like DamnVulnerable Defi and Ethernaut and gradually progressing to participating in audits. Let me know your thoughts or whether I've missed something!!

3 Likes

Day 2: As promised, here is my attempt at a transaction mechanism that includes sanctions:

I think an inherent issue with this code is that although there is a banned list and there is the ability for an owner to remove a user from the banned list, a vulnerability could be that a malicious user attempts to insert themselves as the contract owner.

I believe this sort of issue is touched on in the first Ethernaut challenge. Being relatively new to the space, I'd love to hear from others an explanation about the Fallback challenge on the Ethernaut platform. For ease of reference the code is below:

The challenge is deemed complete if:

  1. you claim ownership of the contract
  2. you reduce its balance to 0

In the interim, I'll be having a go myself and trying to step through it in the next post.

2 Likes

Day 3: I've completed Ethernaut challenge 1 titled 'Fallback' and I've outlined my learnings in chronological order based on the screenshot from my previous post.

TL;DR : Contribute a small amount of wei, call a lowlevel transaction to use the fallback 'receive' function; withdraw funds.

The contract
After reading up on each function, this is my understanding of the contract:

  • defines the contract's name as 'Fallback';
  • the Fallback contract has two properties: 1) a mapping from an address to an unsigned integer that is public (i.e. external participants can contribute a payment to the contact) and 2) the owner's address;
  • 'constructor()' is a function that is executed only when a contract is deployed to the eth blockchain and initialises (basically sets up) the contract's variables / properties;
  • 'owner = msg.send' means whoever deploys the contract is the owner;
  • 'contribution' as a property of the constructor means that the owner must initially contributed 1000 ether to the contract;
  • 'modifier onlyOwner' is a modification to the function of a contract either before or after it is called. In this case, it is used to impose a requirement (hence the use of 'require') that the msg.sender is the contract owner. If this is not the case, the code outputs the error message 'caller is not the owner'. A new learning for me was that the underscore code line simply means the function that this modifier is applied is called as is (i.e. without an error message);
  • 'contribute' function, that contains 'public payable' visibility, is a function that allows internal users, external users or even external contracts to call on this function and pay an amount less than 0.001 ether (otherwise it will revert);
  • 'contributions[msg.sender] += msg.value' simply adds a users contribution to the owners contribution BUT if you contribute more than the initial owner, you become the owner (I initially thought this might be the solution but read 'Solution' below as to why it isn't);
  • 'function getContribution()' is a public viewable function that means it does not change the contents of the smart contract and it can be called without any cost to the contract. When this function is called, it also gives a user the amount of contributions they have made;
  • 'function withdraw' again, a public function that only the owner can call (per the onlyOwner modifier described above). This turns the owner's address into a payable address whereby the owner can transfer the balance of funds to;
  • 'receive() external payable' only an external party can call this function to pay an amount higher than 0 and their contributions have to be >0. An interesting point to note is there is no 'function' specification before 'receive'. After some reading, it turns out this is a fallback function!! It is executed whenever a user / the owner sends money to the contract without any other function being called. If both of these conditions are satisfied then the contributor becomes the owner.

Solution
As mentioned above I first thought the solution would be to simply contribute more than the owner to become the owner and then withdraw the funds. I saw this practically playing out by using the same address to contribute a large volume of small amounts of ether (i.e. <0.001 eth). As I started to work through this I realised this was very time intensive and likely to cost more due to price fluctuations.

To trigger the fallback function, first make a really small contribution to satisfy the user contribution being >0 (via the contribute function). Then send another low amount of eth via sending a transaction (this calls the receive fallback function). This admittedly took a while to arrive at (and I arrived there via Remix IDE). I kept trying to call the receive function explicitly via 'await contract.receive' but from what I understand, because this contract doesn't explicitly define the function, you can only trigger this via a transact function.

Feel free to pick apart the above if I've made any mistakes / can provide any further explanation on the 'contract.sendTransaction'. I feel coming into this field it would be nice for someone to do a step-by-step guide in a reasonably accessibly way. Time permitting, I'll be sure to have a go at the next Ethernaut challenge 'Fallout' I'm super excited.

2 Likes

Day 4: I've now completed Ethernaut challenge 2 'Fal1out'. See the code below and for brevity I'll defer to my first Ethernaut post regarding reading similar parts of the contract:

The Contract

  • a general note that the Solidity version is older than the last challenge (0.6.0 vs 0.8.0);
  • the OpenZepplin SafeMath smart contract is imported. The effect is that the SafeMath library validates if an arithmetic operation would result in an integer overflow/underflow (e.g. overflow occurs when you attempt to store inside an integer variable a value that is larger than the maximum value the variable can hold). If it would, the library throws an exception, effectively reverting the transaction. You don't need the SafeMath library for Solidity versions >0.8;
  • 'Allocate' function saying you can pay some money and it will be added to your allocations.
  • 'sendAllocation' requires the allocation being sent to be greater than 0 but also allows an allocator to transfer the amount (even on another user's behalf) to another user;
  • 'collectAllocations' allows only the owner (via the modifier) to withdraw the funds; and
  • 'allocatorBalance' checks the balance of the allocator.

Solution
This was a bit easier than the last challenge but also a touch annoying. The first thing I did was put the contract into Remix IDE, and a parse error flashed up as a result of the SafeMath contract not importing. After going to the OpenZepplin library and finding the correct version of the contract, importing it into Remix, the contract compiled.

I deliberately didn't speak to the constructor function above because on its face this was not how you use a constructor in Solidity, it should look like: ' Constructor() {.....} '. I then noticed by interacting with the incorrectly spelt constructor that if you called the function, you became the owner of the contract, therefore being able to withdraw all the funds contained therein.

The takeaway for me is making sure constructors are correctly specified and have a work colleague or third party assist you in reviewing code before its rollout to a public audience.

2 Likes

Day 5:
The relevant code for Ethereum Challenge 3, 'CoinFlip' is below:

Contract

  • 'consecutiveWins' is a function that is publicly available to call that counts how many times a user has correctly guessed a coin toss;
  • 'lastHash' provides a value of the last hashblock created (later used to derive blockValue);
  • 'FACTOR' provides a 'unique' factor that is used to divide the blockValue, ultimately resulting in a coinflip that is supposedly random;
  • the constructor sets the value of consecutive wins as 0;
  • the 'flip' function contains a boolean parameter (a value that returns TRUE or FALSE) and is publicly viewable (i.e. whether a user's guess is correctly, heads or tails);
  • the value of blockValue is determined as its current hash, which is determined relative to the position of the last block hash. This was a pretty confusing point for me at first but the way I figured it out was: blockValue aims to make sure that a new hash is not a repeat of a past block (i.e. a user can't keep copying the last block, saying they have correctly predicted 10 instances - if this is the case, the contract reverted via 'if (lastHash == blockValue)'; and
  • 'consecutiveWins' are added as the user correctly enters a '_guess' that is equivalent to a side (with side being determined as whether a coinFlip is equivalent to 1 (again, determined by the blockValue divided by the 'random' factor).

The Solution
I had to look this one up because I didn't realise 1) I would need to create a new smart contract and 2) despite understanding the vulnerability of 'fake randomness', I wasn't sure how to exploit it. The code I wrote looked like this:

CALL TO ACTION!!
Let me know your thoughts on how to deploy this. The video I watched created a script that deployed a similar answer 10 times as opposed to doing this 10 times on Remix IDE. I'd really appreciate some guidance on how to do this!!

2 Likes

What exactly does "deploy an answer" even mean?

If you meant - deploy a contract, then you don't need to deploy it more than once, since the actual functionality that you want to execute is implemented in a callable function, not in the constructor.

So you only need to deploy this contract once, and then you can call that function however many times you want.

1 Like

Poor use of phrasing on my part - I've currently deployed both the CoinFlipAttack contract and Ethernaut's coinFlip contract. As you've correctly pointed out, I can call the flipAttack function as many times as I want. From my understanding, I need to call this function 10 times to complete the challenge (such that the condition of 10 consecutativeWins is satisfied).

I wondered if there was a script in Python (or other language) that I could use to automate this process as opposed to manually: 1) calling the flipAttack function, 2) paying test eth to complete this, 3) waiting for the mint process and, repeating.

Any suggestions?

2 Likes

You can use web3.py in order to write such script in python.
You can use web3.js or ether.js in order to write such script in javascript.
You can use web3j in order to write such script in java.
And I bet that there are several other options in several other languages.

You could also just do the whole thing in the constructor (repeating it there 10 times), which will leave you with a single transaction to execute, which isn't much of a big deal to do even manually from remix.

On top of that, doing so will probably also save you quite a bit of gas, compared with executing it in 10 separate transactions.

And lastly, you will not be subjected to being frontrun by someone else doing the same thing with a higher gas-price.

1 Like

I was thinking Brownie-eth for Python but I like the idea of becoming more competent in web3.py so I may try looking there, thanks!

On the idea of repeating it 10 times in the constructor, would there be a risk of opening the contract up to constructor abuse (maybe like a reentrancy attack?). If not, could you please explain.

Could you also please elaborate on your last sentence, that's a really interesting point I hadn't considered. I understand that if a user would be willing to pay more gas then their contract would execute before yours but how would that practically occur in a scenario like this?

2 Likes

The one place where a reentrancy attack can NEVER take place no matter what - is the constructor.

The code in this function is executed once and only once (upon contract deployment).

Reentracy by definition means that a function is executed again.

2 Likes

For example, you execute your transaction 10 times, specifying a gas price of N wei-eth per gas unit.

Another user runs a server which monitors the chain, and immediately after detecting your 9th transaction, sends its own transaction, specifying a gas price of N+1 wei-eth per gas unit, thus frontrunning your 10th transaction.


Technical note:

The example above depicts a process which is executed with gas-scheme type 1 (the original gas scheme), where only one of the transaction's configuration parameters is related to the gas price - gas_price.

Most transactions today are executed with gas-scheme type 2 (EIP-1559), where two of the transaction's configuration parameters are related to the gas price - max_priority_fee_per_gas and max_fee_per_gas.

3 Likes

Thank you for your patience in explaining both the constructor point and especially the gas scheme types!

3 Likes

Day 6: I've reflected a bit more on this CoinFlip challenge and although frustrating at some points, I really enjoyed digging into the detail for this one. Shoutout to barakman for fielding my queries, it made me stop and think a bit deeper and it certainly told me I need to work on how I phrase some questions.

A key takeaway for this challenge for me was that nothing is ever truly random in smart contracts provided everything is publicly visible (including variables marked as 'private' which was a surprise!). I'll be sure to push on with challenge 4 'Telephone', but I also want to look at the docs for web3.py given my familiarity with python in an attempt to understand how the language interacts with the blockchain via frameworks like 'eth-brownie'.

1 Like

Nothing is truly random because the entire current state is known at any given moment, so one can tell the exact outcome of a transaction (i.e., the entire future state following that transaction), making everything fully predictable, thus not random.

The restriction-level of state variables - public, internal and private - is only a technical feature of the compiler, designated for supporting better encapsulation (and more generally, better coding design).

Declaring a state-variable as internal or as private instead of as public only makes it "just a little technically harder" to read.

But with a bit of technical knowledge about storage slots, one can easily look into the chain and obtain the current value of any state-variable.

2 Likes

Well put, this is exactly the point that very same challenge was driving home.

Picking up on that last point you made, further readings suggest that the use of a randomness beacon and verifiable random function (VRF) is a counter to these sorts of hacks. In particular, Chainlink VRF, which is a service that users to obtain a very close approximation to a truly random number from a decentralized source.

The primary article I think explains randomness and its implications is found here. I'm part way through and find it to be pretty interesting but requires the occasional google for some concepts.

1 Like

Day 7: I completed the Ethernaut 'Telephone' challenge. Contract details are as follows:

Relatively straightforward, the only nuance being changeOwner. In this public function, the address of the contract's owner may be called by external parties. The owner is changed if the transaction origin (denoted by tx.origin) is the same as the message sender (msg.sender). As a quick guide - the tx.origin can be your Metamask wallet whilst the msg.sender is the address of whatever called the currently executing smart contract.

My exploit:

In my solution, the constructor generates a contract that has the Telephone contract's address. Once this contract is deployed, a user is able to call 'changeOwner' function and the challenge is completed. This was scarily simple and showed me how messing up either a msg.sender or tx.origin can costly and prone to phishing-style attacks.

1 Like

Day 8: Being a Friday, I wanted to get stuck into some light reading so I decided to look at the overall structure of an audit report from none other than OpenZepplin. I read the Forta Protocol Audit Report and it may seem basic but I learnt the general structure of a report.

Before getting into my key takeaway from the report, its important to understand what Forta does. In brief, Forta specialises in permissionless network of threat detection based on the usage of agents (now called “detection bots”), scanners, and analyzers over a multi-chain platform to report alerts once a potential threat to a project has been detected.

The high severity issue that I was interested in was that a malicious user can register a scanner under any owner. What this means in the context of a report is rather than a user going through a registration process outlined in a contract, the '_register function' was marked as a public function. The implication being that any user could skip the checks and register a scanner in the same way as the admin does it. In light of my recent Ethernuat challenges, it was really cool to see this play practically and awesome to see that the suggestion from the OpenZepplin team was implemented relatively quickly by Forta.

1 Like

Well, the most basic part in ANY audit, is to look for public and external functions which do not impose any access-restriction (can be called by anyone).

Which is exactly what you've described here, but with many more details.

It's like literally the very first thing that any auditor looks for when auditing a contract.

That's not to say that such functions are prohibited under any circumstances, but rather just to say that this is the very first thing to inspect closely when you look for vulnerabilities in a contract.

Day 9: Reading OpenZepplin's audit report (plus barakman's comment on the _register function) got me thinking more about what sort of things would an auditor need to do when conducting an audit. I know this might seem like an obvious point to make but I want to be clear on how unit testing works as a fundamental concept before I start practicing audits.

I looked into how auditors conduct unit testing via frameworks like Slither-analyser, hardhat etc. I admittedly have no real grounding in any sort of unit testing so I started with Slither. After going through the process of installing a new Hardhat.js project (see here for more information) I decided to install Slither with the intention to apply what I've learnt to contract I've created.

As it currently stands, I've installed Slither but I'm finding it difficult to actually run the tests. For those interested this is the initial setup of Slither once you install it and its respective extension into VS code:

I won't breakdown the current Lock.sol contract provided I'm going to edit it to test my GodModeToken and potentially a mock-up Dapp I've been working on in the background of these posts. I will be sure to post the results of the unit test tomorrow once I've done further readings. If people wanted to reply with resources that helped them better understand unit testing, that would be much appreciated!!

1 Like