# Guardian - Solidity Lab

Securing the Bleeding Edge of Blockchain

Welcome to the Solidity Lab!

This is a community aimed at building and sharing a wealth of blockchain and solidity knowledge to help developers/auditors of all levels transform the web3 ecosystem.

Apply to join [here](https://docs.google.com/forms/d/e/1FAIpQLScHMxP3LTU96heRoV8DHeEEK1YIKqtbOWcT4cK9lAUkx0ZyUQ/viewform) ⬅️

### What Is Solidity Lab?

In Solidity Lab you'll get the opportunity to:

* Share and learn from others interested in Solidity security
* Get direct answers about the latest security practices from active auditors
* Access curated security-focused Twitter posts, blog articles, videos, and more
* Get daily puzzles and "spot-the-bug" challenges
* Team up with others and participate in practice audits
* Join professional auditors in a "shadow audit" of a real blockchain project

On this site you'll find:

* An Encyclopedia containing information on all known Solidity attack vectors
* An Encyclopedia containing information on all known common Solidity bugs
* The Auditor's Handbook, a collection of guides for all things auditing

### The Vision

Empower an open, trustless, decentralized transaction layer; that anyone can build on with widespread access to the resources necessary to protect against bugs and exploits.

### The Mission

To accomplish the vision, the community must align on cultivating world-class auditors at scale.

We must radically adhere to our core values and take every chance to make it easier to access, study, and practice Solidity security.

Each and every community member is the most valuable resource at hand.

### The Core Values

* Thorough Without Exception
* Long Term Oriented
* Leverage Focused
* Constantly Curious
* Vehemently Passionate
* Inherently Collaborative

<details>

<summary>Contributing</summary>

The Vision and Mission can only be achieved through fervent collaboration. Every member of Solidity Lab is encouraged to give back and contribute whatever they can to enrich the experience for others.

You'll find that by giving more, your own experience is enhanced.

Contribute to this knowledge base by opening a pull request on [Github](https://github.com/GuardianAudits/SolidityLab). Upon review, your change request will be merged and you will receive the `Contributor` role.

</details>


# Encyclopedia of Solidity Attack Vectors

Every known solidity attack vector.

## Attack Vectors

{% content-ref url="/pages/eWbF1cQ1vKoPEwWUjvfn" %}
[Reentrancy](/encyclopedia-of-solidity-attack-vectors/reentrancy)
{% endcontent-ref %}

{% content-ref url="/pages/vDTrCIzGOXVwQtetk2AQ" %}
[Contract Cannot Accept Ether DoS](/encyclopedia-of-solidity-attack-vectors/contract-cannot-accept-ether-dos)
{% endcontent-ref %}

{% content-ref url="/pages/TO29AEaBArqTdiiEiYq8" %}
[Gas Griefing](/encyclopedia-of-solidity-attack-vectors/gas-griefing)
{% endcontent-ref %}

{% content-ref url="/pages/4fPklQLHopVCDWUUhjol" %}
[isContract Manipulation](/encyclopedia-of-solidity-attack-vectors/iscontract-manipulation)
{% endcontent-ref %}

{% content-ref url="/pages/WA0bSn5QAgslzK3Ffdxt" %}
[🏃♂ 🏃♂ Front/Back-Running](/encyclopedia-of-solidity-attack-vectors/front-back-running)
{% endcontent-ref %}

{% content-ref url="/pages/8rpE92icba2TehtyXRvI" %}
[External Call Reverts DoS](/encyclopedia-of-solidity-attack-vectors/external-call-reverts-dos)
{% endcontent-ref %}

{% content-ref url="/pages/EN2RurZWt0yrhCzDlByt" %}
[block.timestamp Manipulation](/encyclopedia-of-solidity-attack-vectors/block.timestamp-manipulation)
{% endcontent-ref %}

{% content-ref url="/pages/GHgVaf3KzK3X33HOEcWP" %}
[tx.origin Phishing Attack](/encyclopedia-of-solidity-attack-vectors/tx.origin-phishing-attack)
{% endcontent-ref %}


# Reentrancy

{% embed url="<https://twitter.com/0xowenthurm/status/1615845670879137796?s=46&t=CcQT3YQafLTbN6MI3tXS3w>" %}
Guide on advanced reentrancy
{% endembed %}

{% embed url="<https://twitter.com/bytes032/status/1616357019522400256>" %}
Guide on read-only-reentrancy
{% endembed %}


# Contract Cannot Accept Ether DoS

This article explores a specific DoS vulnerability known as "Contract Cannot Accept Ether".\
We'll delve into how attackers exploit this weakness and explore strategies to fortify your smart contracts against such attacks.

## What is DOS ?

Denial-of-Service (DoS) attacks aim to disrupt the normal operation of a contract, preventing legitimate users from interacting with it.\
Smart contracts, while powerful tools for decentralized applications, can be susceptible to those malicious attacks.<br>

## Example

Let's use the famous example from the Ethernaut's CTF.<br>

The Goal of this contract is to be and stay the King.\
It's just an auction contract, whoever pays more than the previous bid get to be the new King.\
The old king will then receive the money sent by the new King

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

contract King 👑 {

  address king;
  uint public prize;
  address public owner;

  constructor() payable {
    owner = msg.sender;  
    king = msg.sender;
    prize = msg.value;
  }

  receive() external payable {
    require(msg.value >= prize || msg.sender == owner);
    // Will fail if the King is a Smart Contract and can't receive Ether
    payable(king).transfer(msg.value);
    king = msg.sender;
    prize = msg.value;
  }
}
```

By default, smart contracts don't accept Ether.\
For accepting them, there's a need for a `receive` and/or `fallback` function.\
The problem with this contract is the fact it assume that the new king is a EOA (i.e. a Wallet) and can accept Ether. However is the next example we can see that the caller isn't a EOA but instead a smart contract that can't accept Ether. It will revert if the user's contract look like this :

```solidity
contract CannotAcceptETH{
  constructor(address _kingContract) payable {
    payable(_kingContract).call{value: msg.value}("");
  }
}
```

Indeed, the above contract has no `receive` nor `fallback` function.

From there the contract is Doom as soon as this contract becomes the new King.\
The King will stay King forever but could never get back the money they bid.<br>

## Mitigation and Best Practice:

In this example, to prevent this we could :\ <br>

* Give access to the `owner` to remove the current king (but it would not be decentralized):

```solidity
    // Emergency function to reclaim the throne if Ether transfer fails persistently
    function reclaimThrone() external {
        require(msg.sender == owner, "Only the owner can reclaim the throne");
        king = owner;
    }
```

* Only accept ERC-20 like WETH, that will not trigger a revert.

This kind of Attack through DOS is one of many ways things can go wrong. A good rule of thumb while auditing is to:\
***Be extra careful when calling an arbitrary address.***

## Conclusion:

This was an example of a User contract that can't accept Ether and DOS the King contract.

But it's quite frequent to see protocols that forgot to set up a `receive` and/or `fallback`, so no one can send Ether to the contract ever.


# Gas Griefing

{% embed url="<https://twitter.com/0xOwenThurm/status/1618417556683108352>" %}


# isContract Manipulation

## The concept

There are two types of accounts in the Ethereum world.

1. EOA Externally Owned Accounts, these are users of wallets
2. Contracts, these are deployed smart contracts,

Sometimes there is a requirement to check if the caller is a EOA or a Contract.\
\
The code for this in solidity is to use inline assembly as below.\ <br>

```
assembly {
    contract_codesize := extcodesize(who_is_calling)
}
```

\
\
The code checks if the address at `who_is_calling` has code associated with it.\ <br>

There is a way to make a malicious contract seem as though there is no code associated with it, and that is by calling the vulnerable contract and function from within the malicious contract's constructor.\
\
Below is a vulnerable contract and a test contract to test this concept. We will be using anvil from the Foundry suite to deploy from Remix.\ <br>

```javascript
// SPDX-License-Identifier: Unlicensed
pragma solidity 0.8.18;

contract VulnerableContract
{
    function notcontracts() public view returns (uint256 somethingReallyValuable)
    {
        //assume all checks are done
        uint256 contract_codesize;
        address who_is_calling;
        who_is_calling = msg.sender;
        //First check the extcodesize of the caller
        assembly {
            contract_codesize := extcodesize(who_is_calling)
        }
        if(contract_codesize > 0)
        {
            //THIS IS A CONTRACT CALLING SO THEY CAN'T GET OUR SUPER VALUABLE ASSET
            somethingReallyValuable = 0;
        }
        else
        {
            //THIS IS A USER CALLING SO THEY CAN GET ALL OF OUR SUPER VALUABLE ASSET
           somethingReallyValuable = type(uint256).max; 
        }
    }
}

contract testCodeSize{
    uint256 public allTheirValue;
    address public vulnerablecontract;

    //When the constructor runs the extcodesize is still 0
    //lets see what allTheirValue is after the constructor
    //allTheirValue should be uint256 Max value.
    constructor(address _vulnerablecontract){
        vulnerablecontract = _vulnerablecontract;
        allTheirValue = VulnerableContract(vulnerablecontract).notcontracts();
    }
    //This function should set allTheirValue to 0
    function wontWork() external {
        allTheirValue = VulnerableContract(vulnerablecontract).notcontracts();
    }


}
```

In the `testCodeSize` there is a variable called `allTheirValue`, this will hold the value that is retrieved from the Vulnerable contract, if the call is able to bypass the check for code size we should see the `allTheirValue` variable holding a very large value, however if it fails the value should be "0". In the `testCodeSize` contract, the call to `VulnerableContract.notcontracts()` is made from within the constructor and an external function called `wontWork()`.\ <br>

Below are screenshots of the value held by `allTheirValue` after deployment and then after calling the `wontWork()` function.\ <br>

First deploy `VulnerableContract`.\
Copy the deployed address of `VulnerableContract` and deploy the `testCodeSize` contract.\
Directly after the deployment of the `testCodeSize` contract, the value of the `allTheirValue` variable holds a really large value.\
\
![valueofallTheirValue1](https://2713856283-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWRuBMx8L1wxnbq5Ahdl2%2Fuploads%2Fgit-blob-2d232279aee0e1fa2dde5c5c4a84c970e12b2e3a%2FvalueofallTheirValue1.png?alt=media)\
\
After calling `VulnerableContract.notcontracts()` the value of the `allTheirValue` variable is now "0" as the `testCodeSize` contract now has code associated with it.\
\
![valueofallTheirValue2](https://2713856283-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWRuBMx8L1wxnbq5Ahdl2%2Fuploads%2Fgit-blob-5dff17767a04d05615fde8badd34d8abdd5b5281%2FvalueofallTheirValue2.png?alt=media)\ <br>


# 🏃♂ 🏃♂ Front/Back-Running

## Introduction

In the fast-paced world of Web3, transaction speed can be a game-changer.\
However, this very characteristic introduces vulnerabilities like Front Running and Back Running.\
These exploits leverage the Mempool and gas fee system to manipulate transaction execution order for personal gain.

## Important Terminology:

***Mempool***: This is a temporary holding area for transactions (tx) waiting to be validated. All these tx are publicly available to read by anyone before they are validated.\
***Base Fee***: This is the minimum fee required for transactions (tx) inclusion based on network congestion.\
***Priority Fee***: This is an optional fee users pay to validators to incentivize faster transactions (tx) processing.<br>

## Analogy

The ***Mempool*** in a crowded coffee shop is the line of customers waiting.\
The ***Base Fee*** is the coffee's minimum price, rising with demand.\
The ***Priority Fee*** is like tipping the barista to get your coffee first, jumping the queue."

So the higher your `priorityFee` is, the faster your tx will be executed.

A ***Front Run*** tx is submitted after the user's tx but gets validated before the user's tx.

A ***Back Run*** tx is submitted after the user's tx but gets validated after the user's tx.

The Front Runner will see your submitted tx in the mempool and simulate it.<br>

If there is a way that they make money, the Front Runner will pay higher ***Priority Fee*** so their tx will be validated first.

## Example of Attacks

### Front Running

In this example in Ajna, a user can deposit NFT as collateral.\
Alice wants to deposit a Crypto Punk as collateral in order to take a Loan from Ajna.<br>

However, the `CRYPTOPUNKS` don't follow the ERC-721 standard.\
So following the `CRYPTOPUNKS` contract, the user has to Offer his `CRYPTOPUNK` for Sale to a specify Address, and then the address can buy it from the user.

1. Alice's Initial Transaction: Alice call the `CRYPTOPUNKS::offerPunkForSaleToAddress` function and specify the Ajna Address.
2. Alice's Second Transaction: Alice now send a tx, calling the `Ajna::addCollateral` which is going to buy the `CRYPTOPUNK` from Alice and accept her collateral.
3. Bob's Front Run: Bob sees that tx, and Front Run Alice's second tx, so the Ajna Protocol is indeed buying the token from Alice but as Bob initiate the tx, the collateral will be accepted in his name.
4. Bob has now two options
   1. He can have a Loan for free, so never repay back because the collateral was free.
   2. He can withdraw back the collateral and get the `CRYPTOPUNK` back.
5. Alice has lost her `CRYPTOPUNK`.

![bob-alice-front-run](https://2713856283-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWRuBMx8L1wxnbq5Ahdl2%2Fuploads%2Fgit-blob-b33e303f46185650b7c314a69f642c208bf27e65%2Falice-bob-front-run.jpg?alt=media)

Remediation: Before buying back the NFT from Alice, the protocol should verify that she is the one who initiated the transaction.

Note that the Front Runner himself can be Front Run by another one if ***Priority Fee*** is even higher.

[Front Run Attack Link](https://solodit.xyz/issues/h-3-cryptopunks-nfts-may-be-stolen-via-deposit-frontrunning-sherlock-ajna-ajna-git)

### Sandwich Attack

A Sandwich Attack is a form of Attack when an Attacker will Front Run & Back Run your tx.<br>

![sandwich-attack](https://2713856283-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWRuBMx8L1wxnbq5Ahdl2%2Fuploads%2Fgit-blob-27742b1147889cdb5881557e50f0e0c03a4b21c4%2Fsandwich-attack.jpg?alt=media\&token=41701557-22a4-43f6-b114-41e80bc4d073)

The idea is to place the smart contract in a state just before the victim's tx goes through and place another tx just after.

In this Attack, there's a function that has improper access control.\
Therefore anyone can call it. This function make the Protocol to Buy some CRV token on Uniswap.<br>

1. Bob takes out a flash loan and buys CRV tokens, causing the price to rise 📈
2. Bob executes the Mochi Contract to buy some CRV tokens on Uniswap, causing the price to rise 📈
3. Bob's CRV tokens are now worth much more, allowing him to resell them at a higher price.
4. The Mochi Contract purchased CRV tokens at an inflated price, benefiting Bob.

![sandwich-attack-bob](https://2713856283-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWRuBMx8L1wxnbq5Ahdl2%2Fuploads%2Fgit-blob-5b33a441857ca9a13582a9b2ca155cce16d991bd%2Fsandwich-attack-bob.jpg?alt=media\&token=eff1dcaf-c0f1-437d-a2a4-3102f94488ff)

![sandwich-attack-graph](https://2713856283-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWRuBMx8L1wxnbq5Ahdl2%2Fuploads%2Fgit-blob-9c051a97bab4d1a282a308ad68a91a24fd715fc4%2Fsandwich-attack-graph.jpg?alt=media)

Remediation: This was an improper access control. A regular user shouldn't have access to this function. Set a `onlyOwner` modifier.

[Sandwich Attack Link](https://solodit.xyz/issues/h-09-treasury-is-vulnerable-to-sandwich-attack-code4rena-mochi-mochi-contest-git)

## How to prevent them ?

### FlashBots

Flashbots are a private Mempool that submit groups of transactions directly to validators, bypassing the public mempool. Some of the validators agree not to publish the transactions submitted to their node publicly.

Pro:

* ✔️ They can help prevent Front Running and Back Running by allowing users to prioritize their transactions.

Cons:

* ❌ They tend to be slow, because you'll need to wait until it's one of the validator's turn to validate a block.
* ❌ There is a part of centralization and trust. You have to trust that those validator are not going to Front Run you.

### MEV Awareness

* Understanding MEV risk: Knowing that these exploitative tactics exist puts you on guard. discourages front-runners while ensuring your transaction gets processed.
* Identifying smart contract vulnerabilities: Awareness of MEV can lead you to identify potential vulnerabilities in your contract that attackers could exploit through MEV techniques.

## Conclusion

Front-running and Back Running are challenges in the fast-paced world of Web3.\
By understanding MEV (Maximal Extractable Value) you can protect your transactions.\
As Web3 evolves, new solutions like Layer 2 scaling and flashbots offer hope for a future with less MEV risk.\
Stay informed to navigate this dynamic landscape.

## Additional Resources

Best Article on MEV: [Ethereum is a Dark Forest](https://www.paradigm.xyz/2020/08/ethereum-is-a-dark-forest),\
Others Articles: [MEV: DeFi Transaction Ordering for Profit and Fun](https://mixbytes.io/blog/mev-defi-transaction-ordering-for-profit-fun),\
[Solidity By Example](https://solidity-by-example.org/hacks/front-running/),

Mempool Live Visualization: [Ethernow](https://www.ethernow.xyz/), [tx Town](https://tx.town/v/eth)


# External Call Reverts DoS

External calls can cause the contract to be vulnerable to DoS attacks. To better explain how can this be possible, consider the following simplified Auction contract:

```solidity
contract Auction {

  address public currentOwner;
  uint public currentBid;

  constructor() payable {
    currentOwner = msg.sender;
    currentBid = msg.value;
  }

  receive() external payable {
    require(msg.value > currentBid);
    payable(currentOwner).call{msg.value}("");
    currentOwner = msg.sender;
    currentBid = msg.value;
  }
}
```

For anyone to be a new owner of the Auction, he needs to send an amount of ether greater than the current price (which is set by the `currentOwner`).\
To prevent someone else from being a new owner (even if he has more ether than the current price), we can perform a DS attack into the contract by creating a malicious contract that we register as the `currentOwner` (by sending ether greater than the current price of course) and reverts the transaction whenever it receives ether. So, when a new address attempts to be a new owner (the `currentOwner` is our malicious contract), the transaction will revert, hence the `newOwner` will not be set anymore.\
Below is an example of a Malicious contract that can perform a DoS attack on the Auction contract:

```solidity
contract AuctionDOS {

    constructor(address payable _auction) payable {
        uint currentBid = Auction(_auction).currentBid();
        require(msg.value > currentBid, "You need to send more ether to be the currentOwner");

        // we register the contract as the currentOwner
        (bool success,) = _auction.call{value: msg.value}("");
        
        require(success, "Failed to register as currentOwner");
    }

    receive() external payable {
        // the contract will revert the transaction whenever there is an attempt to change the currentOwner
        revert("newOwner can not be set anymore x)");
    }
}
```


# block.timestamp Manipulation

{% embed url="<https://twitter.com/0xOwenThurm/status/1614289583679868928>" %}
In depth guide on `block.timestamp` manipulation
{% endembed %}


# tx.origin Phishing Attack

## tx.origin phishing vulnerability

tx.origin and msg.sender can be used to get the address of the account making the call, however there is extra context that is vital to keep in mind.

We have 3 components in our example

1. Alice, the address of Alice's wallet in our example is 0x01 (The Depositor).
2. Bob, Bob has deployed a malicious contract in our example at address 0x02
3. The vulnerable contract, Pool, this is deployed in our example at address 0x03\
   \
   In each scenario the Depositor address should be `0x01` to be able to withdraw funds from the Pool contract.\ <br>

Here is the Vulnerable contract, note the check for access control in the `withdrawfunds()` function uses `tx.origin`\
\
![Pool](https://2713856283-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWRuBMx8L1wxnbq5Ahdl2%2Fuploads%2Fgit-blob-e4cd82351fe6287faeea3052fa73d5fbb1c85e92%2Fpool.png?alt=media\&token=1d29a6a9-2169-43f6-8918-0d78659f8ae7)\ <br>

Below is Bob's malicious contract.\
\
![Mal](https://2713856283-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWRuBMx8L1wxnbq5Ahdl2%2Fuploads%2Fgit-blob-5a5f41b4994463b9135a33f8cc010e2ece5c95b9%2Fmal.png?alt=media\&token=8678b6d4-811d-48de-93f1-ff03216a47b3)

Let's first look at the path from Alice to the Pool contract where no malicious actions happen. Alice calls `widrawfunds()` at 0x03, in this instance Alice's wallet address will be both the `msg.sender` and the `tx.origin`, and therefore the funds are sent to the correct caller.\ <br>

If Bob's contract calls the Pool contract in the similar way the code at the line `require(tx.origin == pool.depositer, "Must be the depositer");` will receive 0x02, and the call will revert.\ <br>

Now let's take a closer look at the vulnerability, if Bob can get Alice to call the malicious contract at 0x02, and then pass the call through to the Pool contract deployed at 0x03.

* 0x01 -> calls the `donate()` function at 0x02
* 0x02 passes on the call to Pool function `widrawfunds()` at 0x03\ <br>

At this stage the `msg.sender` is 0x02, which should fail as it's not the original depositor, however the `tx.origin` is still 0x01 as the account that started the transaction will always be Alice's.\ <br>

The check that the `tx.origin` is the depositor will therefore pass and Bob's contract will be able to claim all the funds due to Alice.\ <br>

## Resolution

It is recommended not to use `tx.origin` for access control.


# Directly Sending Funds


# Signature Malleability

In-depth thread here:

{% embed url="<https://twitter.com/0xOwenThurm/status/1619151598877577216>" %}


# Encyclopedia of Common Solidity Bugs

Every known common solidity bug

## Known Bugs

{% content-ref url="/pages/KWp3vXwZXUQzbAlvi9O7" %}
[Division Precision Loss](/encyclopedia-of-common-solidity-bugs/division-precision-loss)
{% endcontent-ref %}

{% content-ref url="/pages/uanCLR6dGtrasDtXnNWr" %}
[Unexpected Panic Revert](/encyclopedia-of-common-solidity-bugs/unexpected-panic-revert)
{% endcontent-ref %}

{% content-ref url="/pages/N12Q8cqqVDhh4yelg1wL" %}
[Matching to/from Addresses](/encyclopedia-of-common-solidity-bugs/matching-to-from-addresses)
{% endcontent-ref %}

{% content-ref url="/pages/Wyl6zTOsRFlN7QCGB6Cu" %}
[Forget to Update Parallel Data Structures](/encyclopedia-of-common-solidity-bugs/forget-to-update-parallel-data-structures)
{% endcontent-ref %}

{% content-ref url="/pages/ncvutVLp7ovWbg64y3yA" %}
[Lack of Success Checks](/encyclopedia-of-common-solidity-bugs/lack-of-success-checks)
{% endcontent-ref %}

{% content-ref url="/pages/Sn0uCsaoVBIU8Qro1H9D" %}
[🤷♂ 🤷♂ Lack of Access Control](/encyclopedia-of-common-solidity-bugs/lack-of-access-control)
{% endcontent-ref %}


# Division Precision Loss

## What are they?

Solidity use Fixed Point Arithmetic, that mean it doesn't support decimal value.\
As a result, any non-integer value is truncated downward.\
This characteristic of Solidity can lead to precision loss during numerical operations, especially when division is performed before multiplication, adversely affecting the accuracy of calculations.

```javascript
For Example in Solidity

3 / 2 = 1;

1 / 2 = 0;

```

## Different Kind of Division Precision Loss

Division precision loss can manifest in several ways within Solidity.\
This article focuses on the two most prevalent issues:

* ***Division Before Multiplication***
* ***Rounding Down To Zero***

### Division Before Multiplication

Solidity truncates any non-integer result to the nearest lower integer.\
If a division occurs before a multiplication, the operation may result in precision loss due to truncation.

```javascript
For Example

The expected result is 55.
Solidity make the first calculation 11 / 2 = 5 due to trucation
Then proceed to multiplication.

uint a = 11;
uint b = 2;
uint c = 10;

a / b * c = 50 instead of 55
```

This is a common rule to follow:

### ***"Always Multiply Before Dividing"***

Although many developers follow this rule, "Hidden Precision Loss" can still occur, resulting from complex calculations across different functions or contracts.\
These scenarios are trickier to identify but pose a significant risk if overlooked.

Let's take an example of the USSD Contest on C4:

```javascript

function rebalance() override public {
      uint256 ownval = getOwnValuation();
      (uint256 USSDamount, uint256 DAIamount) = getSupplyProportion();
      if (ownval < 1e6 - threshold) {
        // @audit amountToBuy is the parameter of this call
        BuyUSSDSellCollateral((USSDamount - DAIamount / 1e12)/2);
      }
}

```

At first glance, the calculation appears correct, let's take a look at the `BuyUSSDSellCollateral` function<br>

```javascript

function BuyUSSDSellCollateral(uint256 amountToBuy) internal {
  CollateralInfo[] memory collateral = IUSSD(USSD).collateralList();
  uint amountToBuyLeftUSD = amountToBuy * 1e12;
  ...
  ...

```

But the `BuyUSSDSellCollateral` function multiplies the input by 1e12, leading to potential precision loss.

So it will first do the calculation inside the parenthesis `(USSDamount - DAIamount / 1e12)/2` Then call the 'BuyUSSDSellCollateral' function and multiply the result by 1e12.\
But mutiply a number that might has been round down by 1 Trillion seems not to be a good idea.

```javascript
If we adjust our example above and change c to 1e12,
The expected result is 5.5e12. However:

uint a = 11;
uint b = 2;
uint c = 1e12;

a / b * c = 5e12

5.5e12 - 5e12 = 0.5e12;

The difference is 0.5e12, indicating a half-a-trillion error. 🤯
```

[Source of findind here](https://solodit.xyz/issues/m-8-buyussdsellcollateral-always-sells-0-amount-if-need-to-sell-part-of-collateral-sherlock-none-ussd-autonomous-secure-dollar-git)

### Rounding Down To Zero

In Solidity, due to the same feature, if the Numerator is Lower that the Denominator, the result will be 0\
In regular math:<br>

If $A < B$ with $A, B > 0$

Then $\frac{A}{B} < 1$

So here, the common rule is:

### ***"Always make sure that the Numerator is greater than the Denominator"***

Here's an example in the Cooler Contest on Sherlock:

```javascript

function errorRepay(uint repaid) external {
    console.log("PrecisionLoss.errorRepay()");
    // If repaid small enough, decollateralized will round down to 0,
    // allowing loan to be repaid without changing collateral
    uint decollateralized = loanCollateral * repaid / loanAmount;

    loanAmount     -= repaid;
    loanCollateral -= decollateralized;
}

```

If `loanCollaterral * repaid` < `loanAmount` -> `decollateralized == 0` That's bad, we don't want that to happen.\
It can be tricky sometime to always make sure of that rule. That how Security Reasearchers came up with a sanity check.

```diff
+  require(decollateralized != 0, "Round down to zero");
```

[Source of findind here](https://solodit.xyz/issues/m-3-repaying-loans-with-small-amounts-of-debt-tokens-can-lead-to-underflowing-in-the-roll-function-sherlock-cooler-cooler-git)

## Conclusion

While division rounding errors might seem minor, they can lead to significant fund risks if overlooked.\
This overview only scratches the surface of common division rounding errors in Solidity. Researcher are encouraged to delve deeper into the subject to understand and mitigate potential precision losses in their audit.


# Unexpected Panic Revert


# Matching to/from Addresses


# Forget to Update Parallel Data Structures

{% embed url="<https://twitter.com/0xOwenThurm/status/1615391378107764738>" %}
Example of parallel data structures falling out of sync
{% endembed %}


# Lack of Success Checks


# 🤷♂ 🤷♂ Lack of Access Control

## What is it even

Access control is the application of constraints on who or what is authorized to perform actions or access resources. In the world of smart contracts, this means, clearly defining what various users of the system are permitted to do. Weak access control or lack of can allow attackers to access critical functions that can lead to loss of funds.\
Imagine an auction contract where anyone can set the auction deadline, or an amm which allows anyone to set the fee or worse—anyone can call the `pause()` function and freeze the entire protocol, halting trading or token transfers. See how this such a big deal?<br>

There are various ways of implementing access control. You can learn more from [openzeppelin docs](https://docs.openzeppelin.com/contracts/5.x/access-control)

## 🧨 Real Case: The HospoWise Hack

An interesting case study is, the **HospoWise Hack** where a public `burn()` function allowed anyone to burn tokens. A lack of access control on a sensitive function led to direct loss of value.

## Internal/private vs public/external functions

This are used to determine the level of accessibility for any function:

* Internal and private function can only be called by the contract
* Public and external functions can be called by the contract but are also exposed to the public ie anyone can call them. See [solidity docs](https://docs.soliditylang.org/en/v0.8.30/contracts.html#function-visibility) for more details

**🐛 What Can Go Wrong?**

### [HIGH: Attacker can steal all tokens as a result of the payWithERC20() function being public](https://github.com/sherlock-audit/2025-03-crestal-network-judging/issues/325)

The function `payWithERC20()` on payment.sol is used to facilitate payments when creating agents and when user makes a top up. The bug happens because this function is exposed to the public and as such can be called by anyone. An attacker can call this function to steal any approved tokens from any address

```solidity
    function payWithERC20(address erc20TokenAddress, uint256 amount, address fromAddress, address toAddress) public {
        // check from and to address
        require(fromAddress != toAddress, "Cannot transfer to self address");
        require(toAddress != address(0), "Invalid to address");
        require(amount > 0, "Amount must be greater than 0");
        IERC20 token = IERC20(erc20TokenAddress);
        token.safeTransferFrom(fromAddress, toAddress, amount);
    }
```

Guess how they fixed this:

Well, just changed the visibility to internal and Voila, code is secure

```solidity
    function payWithERC20(address erc20TokenAddress, uint256 amount, address fromAddress, address toAddress) internal {
    }
```

See how important it is to validate visibility is set correctly.

## [Anyone Can Cancel Market Orders – Missing Ownership Check](https://github.com/solodit/solodit_content/blob/main/reports/Cyfrin/2024-07-13-cyfrin.zaros.md)

The function `cancelMarketOrder` is meant to cancel an active market order. This is a critical function and as such only owner of the order is supposed to call it.

The function is implemented as follows:

```solidity
function cancelMarketOrder(uint128 tradingAccountId) external {
    MarketOrder.Data storage marketOrder = MarketOrder.loadExisting(tradingAccountId);

    marketOrder.clear();

    emit LogCancelMarketOrder(msg.sender, tradingAccountId);
}
```

Note, the implementation fails to check if the caller is the owner of this order. This allows anyone to cancel a market order.

## 🤦‍♂️ Wrong implementation of access control

The following is extracted from [rabbithole contest on c4](https://code4rena.com/audits/2023-01-rabbithole-quest-protocol-contest) The protocol has a function `mint()` which is used to mint receipts. Minting is however not intended for everyone, only the minter address is allowed to do so.

```solidity
    function mint(address to_, string memory questId_) public onlyMinter {
        _tokenIds.increment();
        uint newTokenID = _tokenIds.current();
        questIdForTokenId[newTokenID] = questId_;
        timestampForTokenId[newTokenID] = block.timestamp;
        _safeMint(to_, newTokenID);
    }
```

They clearly know this function is critical hence the `onlyMinter` restriction.

Let's take a look at the `onlyMinter` modifier

```solidity
    modifier onlyMinter() {
        msg.sender == minterAddress;
        _;
    }
```

Do you see the problem?

The modifier is just checking if **caller is minter but does nothing with the result**. ie it's only saying, please check if this two are the same and proceed to execute the function regardless.

The intention to limit this to only minter is there, but a broken implementation allows anyone to mint receipts. The fix is easy, we just need to do something with the result of `msg.sender == minterAddress` in this case, **we should revert if msg.sender is not equal to minterAddress**

```solidity
    modifier onlyMinter() {
        require(msg.sender == minterAddress,"OnlyMinter");
        _;
    }
```

Another variation where implementation is flawed: [C4 finding](https://code4rena.com/audits/2025-01-liquid-ron/submissions/S-395)

## Conclusion

Access control is a critical aspect of smart contract security—especially when dealing with user funds. Poor or missing access control can allow attackers to manipulate contract behavior or drain funds entirely. For protocol developers, it's important to recognize that access control vulnerabilities can take many forms, as demonstrated in the examples above. For security researchers, it's equally vital to thoroughly analyze all privileged functions and ensure proper role restrictions are enforced. **Access control bugs are rarely complex but they’re often expensive.**


# Inaccurate Allowance


# Overflow/Underflow


# Unsafe ERC20 Operations

## **Understanding ERC20 Tokens and the Need for Strict Standards**

Introduced through Ethereum Improvement Proposal 20 (EIP-20), the ERC20 standard outlines key functionalities such as transferring tokens and give allowance.

While the standard has enable a surge in digital asset creation, it comes with its limitations.\
Primarily, ERC20 only suggests guidelines rather than enforceable rules.\
This has led to a varied implementation of these guidelines by developers, which, in turn, introduces inconsistencies.\
A critical aspect of these inconsistencies is the error handling mechanism, tokens might return a false value on transaction failures, while others might revert the transaction entirely.\
This lack of a uniform approach can lead to unsafe operations, as applications might not uniformly anticipate or handle failures, potentially leading to loss of funds or other security issues.

### **The Challenge: No Standard Way to Safely Interact with ERC20 Tokens**

The ERC20 standard revolutionized the ecosystem by providing a blueprint for token creation. However, it left a lot to be desired in terms of security and reliability.\
One of the most pressing issues is the lack of a uniform method for safely interacting with these tokens.\
This gap in the standard has led to a variety of implementations.

## The Core of the Issue

At its core, the ERC20 standard specifies a set of functions and events that a token contract should implement, but it does not dictate how these functions should handle failures.\
\
For instance, the **`transfer`** and **`transferFrom`** functions are used to move tokens between accounts, traditionally return a Boolean value indicating success or failure.\
However, not all implementations adhere strictly to this pattern.\
Some might choose to revert (i.e., throw an error and undo all changes) on failure. This inconsistency can lead developers to make incorrect assumptions about the behavior of tokens, leading to bugs and vulnerabilities in smart contracts that interact with these tokens.

## A Real-World Example

This is how the function interface should look like from the [Original EIP](https://eips.ethereum.org/EIPS/eip-20) :

```solidity
    function transfer(address _to, uint256 _value) public returns (bool success)
```

![EIP20 transfer Function](https://2713856283-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWRuBMx8L1wxnbq5Ahdl2%2Fuploads%2Fgit-blob-345d487a1e2d0b656b131f58e5fbf8e9d08ac695%2Ftransfer-function-EIP20-standard.jpg?alt=media)

To illustrate the potential dangers, let's check issue found in Solodit from JuiceBox's Audit in Code4rena.<br>

### Here's a internal **'\_transferFrom'** function:

```solidity
   function _transferFrom(
    address _from,
    address payable _to,
    uint256 _amount
  ) internal override {
    _from == address(this)
      ? IERC20(token).transfer(_to, _amount)
      : IERC20(token).transferFrom(_from, _to, _amount);
  }
```

In this code, the devs are using a common IERC20 Interface for dealing with regulars ERC20.\
However, as this interface follows the ERC20 standard, it require a boolean return value.

The function will not work for a number of popular ERC20s (USDT, BNB..) as they don't return any value.

This is what the BNB **'transfer function'** looks like:

```solidity
function transfer(address _to, uint256 _value) { // Doesn't return any value
        if (_to == 0x0) throw;
		if (_value <= 0) throw;
        if (balanceOf[msg.sender] < _value) throw;
        if (balanceOf[_to] + _value < balanceOf[_to]) throw;
        balanceOf[msg.sender] = SafeMath.safeSub(balanceOf[msg.sender], _value);
        balanceOf[_to] = SafeMath.safeAdd(balanceOf[_to], _value);                     
        Transfer(msg.sender, _to, _value);
    }
```

As you can see, there's no return value when the transfer is successful or not. It's just **'throw'**(revert) if the condition for trasfering are not met.

[BNB's verified Code from Etherscan](https://etherscan.deth.net/address/0xb8c77482e45f1f44de1745f52c74426c631bdd52#L77-L85)

[Solodit Finding](https://solodit.xyz/issues/m-03-use-a-safe-transfer-helper-library-for-erc20-transfers-code4rena-juicebox-juicebox-v2-contest-git)

## **Implementing SafeERC20 for Secure Token Interactions**

This is where OpenZeppelin's **`SafeERC20`** library comes into play, providing a robust framework for interacting with ERC20 tokens safely.

### How does it work ?

It which allow developers to manage token transfers more securely.\
Unlike standard methods, which fail silently or do not revert on errors, low-level calls enable handling of return values explicitly\
This means that if a token contract does not perform as expected, your contract can detect and handle this situation effectively.

**Using SafeERC20:**

```solidity

 function safeTransfer(IERC20 token, address to, uint256 value) internal {
        _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));
    }

 function _callOptionalReturn(IERC20 token, bytes memory data) private {
        // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
        // we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that
        // the target address contains contract code and also asserts for success in the low-level call.

        bytes memory returndata = address(token).functionCall(data);
        if (returndata.length != 0 && !abi.decode(returndata, (bool))) {
            revert SafeERC20FailedOperation(address(token));
        }
    }
```

So **`SafeERC20::safeTransfer`** will call the token's transfer's function.\
If there is no return from that function (from weird Tokens), it just go through, knowing that it would have revert if there was an error. Else, it check that the return value is a Bool and that it returns True, otherwise it revert.

So now the Library will throw an error if the trasfer fails, so devs don't have to manually check return values anymore.

In contrast, **`SafeERC20:safeTransfer`** abstracts these checks within the library. It handles the intricacies of interacting with different implementations of the ERC20 interface, making your contract more robust and easier to maintain.

### **Using `SafeERC20` Effectively**

Integrating **`SafeERC20`** is straightforward. Here's how you can incorporate it into your development process:

```solidity
    import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
```

```solidity
// And inside your contract
        using SafeERC20 for IERC20;
```

By adopting **`SafeERC20`**, you leverage OpenZeppelin’s extensive testing and community feedback, which can significantly enhance the security of your blockchain applications.\
Remember, in the ever-evolving landscape of blockchain technology, the importance of security cannot be overstated.\
Using tools like **`SafeERC20`** not only protects your projects but also contributes to a safer and more reliable ecosystem.

### Additionals ressources

* [WEIRD ERC20](https://github.com/d-xo/weird-erc20)
* [EIP-20](https://eips.ethereum.org/EIPS/eip-20)


# Lack of Exception Handling


# Duplicate Values


# Asymmetrical Code


# Constant Protocol Parameters


# Storage Not Updated

The Ethereum Virtual Machine has different areas where it can store data with the most prominent being storage, transient storage, memory and the stack.\
State variables are variables whose values are either permanently stored in contract storage or, alternatively, temporarily stored in transient storage which is cleaned at the end of each transaction

see [Storage in smart contracts](https://docs.soliditylang.org/en/v0.8.30/introduction-to-smart-contracts.html#locations)

Smart contracts often rely on state variables to track things like balances, participants, goals, or limits. If those variables aren't updated when something changes, it can lead to incorrect logic, wasted gas, or even denial of service.

Let's take a look at the following code

```solidity
pragma solidity ^0.8.0;

contract Simple {
    uint256 public total;

    function increase(uint256 amount) external {
        total += amount;
        emit totalIncreased();
    }

    function decrease(uint256 amount) external {
        // Intentionally forgetting to update `total`
        // total -= amount;  <-- missing!
        emit totalReduced();
    }
}
```

In the example above, `decrease()` gives the impression that something has changed, but the state variable total remains untouched. This kind of oversight especially when buried in larger, more complex contracts can break entire protocols.

## Real Example: A Fundraising Contract Gone Wrong

Let’s look at a real world example from a fundraising contract. The contract tracks total contributions using a variable called **totalRaised** and allows users to call `contribute()` to donate funds

Whenever users call `contribute()` the following state updates happen

```solidity
    function contribute() public payable nonReentrant {
        require(!goalReached, "Goal already reached");
        require(block.timestamp < fundraisingDeadline, "Deadline hit");
        require(msg.value > 0, "Contribution must be greater than 0");

        //@<truncated for brevity>

        uint256 effectiveContribution = msg.value;
        if (totalRaised + msg.value > fundraisingGoal) {
            effectiveContribution = fundraisingGoal - totalRaised;
            payable(msg.sender).transfer(msg.value - effectiveContribution);
        }

        if (contributions[msg.sender] == 0) {
            contributors.push(msg.sender);
        }


        contributions[msg.sender] += effectiveContribution;
        totalRaised += effectiveContribution;
```

This updates several important state variables:

* The contributer is added to an array of contributors `contributors.push(msg.sender);`
* We update the senders contribution `contributions[msg.sender] += effectiveContribution;`
* we updated the total Raised amount `totalRaised += effectiveContribution;`

If the `fundraisingGoal` is not reached , the contract allows contributors to get a refund by calling `refund()`

**What do you expect to happen with our state variables?** Well let's see how `refund()` is implemented:

```solidity
    function refund() external nonReentrant {
        require(!goalReached, "Fundraising goal was reached");
        require(
            block.timestamp > fundraisingDeadline,
            "Deadline not reached yet"
        );
        require(contributions[msg.sender] > 0, "No contributions to refund");

        uint256 contributedAmount = contributions[msg.sender];
        contributions[msg.sender] = 0;

        payable(msg.sender).transfer(contributedAmount);

        emit Refund(msg.sender, contributedAmount);
    }
```

Now, what is refund is supposed to do, basically we are redoing what `contribute()` did or that's what we expect to happen.

But something sus is happening with our code above, only one of the state variables we had previously seems to be referenced here ie `contributions[msg.sender] = 0;`

The `refund()` function correctly resets the caller's contribution but fails to do 2 very important things

1. The `totalRaised` variable is not updated even though we refund the caller.
2. The array of contributers is not updated to reflect that one contributor has exited

Let's explore the first scenario:

### What's the problem

Even if funds are no longer present, the contract behaves as if they are, enabling logic based on faulty assumptions.

The contract has a function that allows the admin to extend the fundraiser if the goal was not reached and the fundraising deadline has passed.

Do you see where this is going.

The contract will think more money was raised than actually exists.

**🧪 Example**

* Fundraising goal = 50K
* User A contributes 20K
* User B contributes 10K

`→ totalRaised = 30K`

Deadline passes, goal not met.

User B calls `refund()` and gets their 10K back. → `contributions[B] = 0`, but `totalRaised = 30K` (still!)

If the protocol admin later extends the deadline, and a new user contributes 20K, → `totalRaised = 50K` —> goal appears reached!

**But in reality, only 40K is available. The protocol is now working off a false state.**

**The fix?**

```diff
    function refund() external nonReentrant {
        require(!goalReached, "Fundraising goal was reached");
        require(
            block.timestamp > fundraisingDeadline,
            "Deadline not reached yet"
        );
        require(contributions[msg.sender] > 0, "No contributions to refund");
        uint256 contributedAmount = contributions[msg.sender];
        contributions[msg.sender] = 0;
     + totalRaised -= contributedAmount
        payable(msg.sender).transfer(contributedAmount);
        emit Refund(msg.sender, contributedAmount);
    }
```

## 🧠 Storage vs. Memory: Another Gotcha

Well, state variables can come in various forms, think of making changes to a struct in memory instead of storage

Let's see another variation: we have a crowd fund contract.

```solidity
    struct Campaign {
        // Creator of campaign
        address creator;
        // Amount of tokens to raise
        uint256 goal;
        // Total amount pledged
        uint256 pledged;
        // Timestamp of start of campaign
        uint32 startAt;
        // Timestamp of end of campaign
        uint32 endAt;
        // True if goal was reached and creator has claimed the tokens.
        bool claimed;
    }
```

The above struct stores the info about the crowd fund(ongoing campaign) When launched, the goal is set as well as start and end time.

We also define a mapping to track the total pledged as shown below.

```solidity
    // Mapping from id to Campaign
    mapping(uint256 => Campaign) public campaigns;
    // Mapping from campaign id => pledger => amount pledged
    mapping(uint256 => mapping(address => uint256)) public pledgedAmount;

```

Now, let's take a look at the function users would call to pledge an amount.

```solidity

    function pledge(uint256 _id, uint256 _amount) external {
        Campaign memory campaign = campaigns[_id];
        require(block.timestamp >= campaign.startAt, "not started");
        require(block.timestamp <= campaign.endAt, "ended");

        campaign.pledged += _amount;
        pledgedAmount[_id][msg.sender] += _amount;
        token.transferFrom(msg.sender, address(this), _amount);

        emit Pledge(_id, msg.sender, _amount);
    }
```

**Do we see any issues?**

Appears to be ok, but let's examine `campaign.pledged += _amount;`

What happens if we have another function that checks the total amount pledged.Well, the function would return zero.

This is because, even though we do increment the pledged amount whenever `pledge()` is called, this is never stored in storage. we’re modifying a copy in memory, not the actual stored data. No error is thrown but nothing gets saved either.

The line `Campaign memory campaign = campaigns[_id];` is where the issue stems from.

To fix this we just need to ensure this is stored in storage;

```solidity
Campaign storage campaign = campaigns[_id];
```

[See correct implementation on solidity-by-example](https://solidity-by-example.org/app/crowd-fund/)

These are just some of the ways lack of storage update can manifest. see the following report for another interesting case: [Whitelisted accounts can be forcefully DoSed from buying `curveTokens` during the presale](https://github.com/code-423n4/2024-01-curves-findings/issues/1068)

## 🚀 Conclusion

In smart contracts, every state update matters. Forgetting to subtract, delete, or update a value can leave your protocol in a broken state, even if everything looks like it’s working. it is therefore very important to keep track of all state changes and ensure everything is being updated as required.

**Note: just because it compiles doesn't mean it works — ⚠️**


# Delete Item, Unupdated Index


# Native vs Wrapped Handling


# 1⃣ 1⃣ 1⃣ Off By 1


# Encyclopedia of Security Research

Every known common solidity bug

## Common Patterns & Integrations

{% content-ref url="/pages/SCsHbRqdisaHpCtTalAk" %}
[Uniswap](/encyclopedia-of-security-research/uniswap)
{% endcontent-ref %}

{% content-ref url="/pages/IbOmPMj4EtFxGHO61t2k" %}
[LayerZero](/encyclopedia-of-security-research/layerzero)
{% endcontent-ref %}

{% content-ref url="/pages/HFpjWcxdXGsNA6PbJBHC" %}
[Whitelists](/encyclopedia-of-security-research/whitelists)
{% endcontent-ref %}

{% content-ref url="/pages/DH5nMpJZKa8isYDBMVpN" %}
[Interfaces](/encyclopedia-of-security-research/interfaces)
{% endcontent-ref %}


# Uniswap

## Uniswap

## Uniswap V3

## Uniswap V4

### Edge Cases & Exploit Vectors

* Dust left in Uniswap V4 by the end of the callback will cause a revert
* You could maybe censor async actions by entering a callback on the pool before interacting with the victim protocol
* After a zeroForOne swap the active price can be exactly on a tick, but the activeTick will actually be the previous tick. This merely maintains the invariant that active price is ahead of activeTick, but may cause some issues with protocols — especially for 1 tick spacing pools!
* Sync DoS attack where either native or non-native tokens are donated and not synced, see [L-13 Here](https://github.com/GuardianAudits/Audits/blob/main/GammaStrategies/2025-04-14_Gamma_UniswapV4_LimitOrders.pdf)

### Checklist Items

* Hook functions should be permissioned for only the Uni Pool that uses that hook contract!

### Audit References & Resources

Gamma Uniswap V4 Limit Orders: <https://github.com/GuardianAudits/Audits/blob/main/GammaStrategies/2025-04-14\\_Gamma\\_UniswapV4\\_LimitOrders.pdf>


# LayerZero

## LayerZero

## LayerZero Messaging

### Edge Cases & Exploit Vectors

* When sending requests through the endpoint send function, the refund receiver will receive Ether refunds. This address could re-enter, gas grief, DoS etc...
* Many production tokens (e.g., GMX) do not return a boolean from their mint/burn methods. Calling them through LayerZero’s standard [IMintableBurnable](https://github.com/LayerZero-Labs/devtools/blob/aa43d67ec0ca5a1668b9174a3f3a032e6ce8693d/packages/oft-evm/contracts/interfaces/IMintableBurnable.sol), which expects a bool, will revert. For such tokens use an interface without a return value, e.g.:

```solidity
    // SPDX-License-Identifier: UNLICENSED
    pragma solidity ^0.8.22;

    interface IGMXMinterBurnable {
        function burn(address _account, uint256 _amount) external;
        function mint(address _account, uint256 _amount) external;
    }
```

* Shared nonce channels can enable token-level DoS if multiple token flows share the same channel/nonce sequencing. See LayerZero Console Report, Round Three Findings, I-01 on page 90.

### Checklist Items

* Did you check the refundReceiver specified in the send call for DoS, Re-entrancy, Gas griefing?
* Did you verify the mint/burn interface matches the underlying token (i.e., it may not return a bool)?
* Did you verify that nonce/channel design does not let one token flow DoS another through shared sequencing?

### Audit References & Resources

* [Ethena Onchain Minter](https://github.com/GuardianAudits/Audits/blob/main/Ethena/2025-12-02_Ethena_Onchain_Minter_report.pdf)
* [USDT0 OFT](https://github.com/GuardianAudits/Audits/blob/main/USDT0/USDT0_OFT.pdf)
* [GMX V2.2](https://github.com/GuardianAudits/Audits/tree/main/GMX/V2.2)
* [LayerZero Console Report](https://github.com/GuardianAudits/Audits/blob/main/LayerZero/LayerZero_Console_Report.pdf)

## lzRead

### Edge Cases & Exploit Vectors

* If the target lzRead function reverts instead of successfully executing then the messaging channel can become stuck. [Reference](https://x.com/GuardianAudits/status/1934234468841316601)
* Re-orgs can cause a mis-reported owner at the target block if the confirmations is not set high enough
* The delay between what block/timestamp an lzRead function is queried at and when the lzReceive result comes back is a dangerous no-man's land period. When an lzRead result is reported back through lzReceive, know that the result only attests to the state of the target contract/chain a handful of seconds ago, and that state could have changed.
* With smart contract wallets/multisig wallets it's possible that one user owns address 0xA on one chain and a different user owns the same address 0xA on a different chain
* If the gas specified in the options is insufficient for executing the lzReceive invocation upon receiving the lzRead result then it will not be executed automatically and must be manually invoked through the LzEndpoint
* The returnDataSize specified in the options must match exactly the size of bytes returned from the lzRead function always
* Once the lzRead result has been verified by all DVNs then anyone can execute it through the LzEndpoint. A malicious actor could frontrun the lzExecutor and invoke this message with insufficient gas, or while they've put the protocol in some invalid state
* When sending lzRead requests through the endpoint send function, the refund receiver will receive Ether refunds. This address could re-enter, gas grief, DoS etc...
* Be sure to take ample time to think about state across multiple chains and across time, and how actions being taken on any chain at any point in time can lead to a potential invalid state -- especially if state on one chain has not been synced to that of others.

### Checklist Items

* Did you fuzz the target read function to ensure it never reverts?
* Did you check that the confirmations is configured high enough for each individual target chain that can be used?
* Did you consider different owners of the same address across chains?
* Did you check that the gas specified in the options is sufficient to execute lzReceive with the returned lzRead result in all cases?
* Did you check that the returnDataSize specified in the options matches exactly the size of bytes returned in all cases?
* Did you verify that the correct lzRead channel is being used?
* lzReceive must only be callable by the endpoint
* Did you consider malicious executors of the lzRead result message through the lz endpoint? E.g. for insufficinet gas during execution (censorying) or execution while in an invalid state?
* Did you check the refundReceiver specified in the send call for DoS, Re-entrancy, Gas griefing?
* If users are allowed to supply their own options, enforcedOptions should always be combined with the user supplied options

### Audit References & Resources

* [Yuga Labs Shadows 1](https://github.com/GuardianAudits/Audits/blob/main/YugaLabs/2025-01-17_YugaLabs_NFT_Shadows.pdf)
* [Yuga Labs Shadows 2](https://github.com/GuardianAudits/Audits/blob/main/YugaLabs/2025-02-05_YugaLabs_NFT_Shadows_2.pdf)
* [Azuki Animecoin](https://github.com/GuardianAudits/Audits/tree/main/Animecoin)
* [lzRead Blocked Channel Thread](https://x.com/GuardianAudits/status/1934234468841316601)


# Whitelists

Many protocols today implement whitelists for regulatory compliance, here are some common pitfalls and vulnerabilities with such patterns.

## Edge Cases & Exploit Vectors

* Any whitelisted address can use EIP 7702 to set their own account code that allows non-whitelsited accounts to interact with the protocol

Consider the following scenario:

* Assume protocol has a whitelist mapping, where only users who are whitelisted can call function A
* Bob is whitelisted for his address 0xFF
* Bob uses 7702 to set his 0xFF account code to:

```solidity
contract {
     address victimSystem;
    
     function callThis(...) external {
           victimSystem.whitelistedFunction(...);
     }
}
```

* Now anyone can call the whitelisted function through calling callThis on 0xFF

## Checklist Items

* Did you check if EIP 7702 could be used to bypass the whitelist?

## Audit References & Resources

* M0 Uniswap V4 hook review, L-07: [Pectra Upgrade Enables EOAs](https://github.com/GuardianAudits/Audits/blob/main/M0/M0_Uniswap_V4_Hooks_report.pdf)
* Bracket Wrapped Vault Review: M-01: \[TODO]


# Interfaces

Almost every project has interfaces, there are some sneaky issues that can arise from mal-defined interfaces which create issues in production.

## Edge Cases & Exploit Vectors

Whenever an interface does not align with the implementation of the contract being cast as such, this often results in a DoS revert or extreme logical issue.

Most notably, return values are often mis-matching. Whenever an interface expects a return value and none is provided the EVM will revert. Furthermore if a return value of one type is expected, but a different type is received there can be either a DoS or Critical logical error that occurs during decoding the returnData.

## Checklist Items

* Did you check if the interfaces being used for addresses that receive external calls match the actual implementation of that address's implementation of the target function? Function name, parameters, return values.

## Audit References & Resources

MIMSwap: [H-05](https://github.com/GuardianAudits/Audits/blob/main/MIMSwap/2024-03-21_MIMSwap.pdf)

Nunchi SY Token: [H-02](https://github.com/GuardianAudits/Audits/blob/main/Nunchi/2025-11-22_Nunchi_SY_Genesis_Vaults.pdf)


# The Auditors Handbook

An auditor's best friend in the wild world of crypto

This handbook contains in-depth guides and resources on all things smart contract security.

{% content-ref url="/pages/h6jC66gyDh8J0Vs0pDig" %}
[The Auditing Process](/the-auditors-handbook/the-auditing-process)
{% endcontent-ref %}


# The Auditing Process

The all-in-one guide to the smart contract audit process

Find a summary thread here:point\_down:

{% embed url="<https://twitter.com/0xOwenThurm/status/1618386420456099841>" %}

{% content-ref url="/pages/UYkK7b6iXQbuBUbXBvEY" %}
[0⃣ Audit Setup/Preparation](/the-auditors-handbook/the-auditing-process/audit-setup-preparation)
{% endcontent-ref %}

{% content-ref url="/pages/xZyVt7NH1dYuHNLHFgOX" %}
[1⃣ Beginning The Audit](/the-auditors-handbook/the-auditing-process/beginning-the-audit)
{% endcontent-ref %}

{% content-ref url="/pages/SKPbwrDwWr211yltc14T" %}
[2⃣ The Meat of The Audit](/the-auditors-handbook/the-auditing-process/the-meat-of-the-audit)
{% endcontent-ref %}

{% content-ref url="/pages/AjgvsciArz734kS3HCbR" %}
[3⃣ Writing Tests & PoCs](/the-auditors-handbook/the-auditing-process/writing-tests-and-pocs)
{% endcontent-ref %}

{% content-ref url="/pages/yB9LOMgLgu6IJf3aEQsh" %}
[4⃣ Wrapping Up The Audit](/the-auditors-handbook/the-auditing-process/wrapping-up-the-audit)
{% endcontent-ref %}

{% content-ref url="/pages/NJG9rqVUSyMX8A8TEA6r" %}
[5⃣ After The Audit](/the-auditors-handbook/the-auditing-process/after-the-audit)
{% endcontent-ref %}

{% content-ref url="/pages/cqIYKGamEgEpebrffOFf" %}
[6⃣ Addendum](/the-auditors-handbook/the-auditing-process/addendum)
{% endcontent-ref %}


# 0⃣ Audit Setup/Preparation

The ideal setup and prep for an audit

### Logistics

First and foremost are the logistics of an audit. It is crucial to appropriately scope the time and # of collaborators necessary for an audit. Aside from auditor expertise, the main driver of audit quality and resulting security is simply the number of engineering hours invested.

It is always ideal to work with other auditors, no matter the size/complexity of the audit. For simple projects, two auditors are almost always enough. For larger, more complex projects, more auditors may be necessary.

A ballpark rule of thumb is: 1 auditor per \~2,000 SLOC

This is a \*rough\* estimate, for projects with specialized logic such as advanced mathematics or financial concepts, this ballpark goes out the window.

Now, how much time should each auditor invest?

Each auditor should strive to cover the entire codebase and corroborate their findings with their colleagues.

The often mentioned 200 SLOC/hour may apply to simple contracts or decentralized contests where missing bugs/vulnerabilities is fine but doesn't hold for an audit of a large and complex protocol.

As the number of contracts in the system increase, the time necessary to audit each SLOC increases quadratically. For complex projects, this rate may drop to 100 SLOC/hour or even 50 SLOC/hour for especially nuanced/mission-critical code.

Additionally, be sure you are well-rested and have a clear mind throughout the audit. Overworking and not taking care of yourself will harm mental clarity and affect the quality of your auditing.

### Research/Context

Gaining the appropriate context before an audit will save crucial time during the audit that can be used for more substantive review & analysis.

Learn about what the project is trying to accomplish and how it plans to accomplish that goal technically. If there is documentation for the project, pour over it before the audit begins.

Be sure to ask the team to provide any relevant documents such as specs, RFCs, or diagrams.

Learn about similar protocols that have been created and audited or exploited in the past. Read the previous audits or postmortems. E.g. if it is a DEX, research previous DEX models and familiarize yourself with the common vulnerabilities for DEXs.

### Communication

Coordinate with your fellow auditors to pick a communication medium and cadence.

It is extremely helpful to have a group where all auditors can communicate about exploit ideas and share context on areas of the codebase.

A combination of 24/7 chat with scheduled calls to walk through leads on vulnerabilities/braindump has proved to be effective for the team at Guardian.

### Tooling Setup

There are a few tools/resources you'll want to prepare before the audit begins that will help you throughout the process.

Firstly, you'll want to make sure you have an editor that can easily traverse the contracts, using something like IntelliJ or VSCode will allow you to click through and see the definition of each function/contract.

Next, you'll want to set up a findings document that you can share amongst all the auditors collaborating. The findings document acts as a bank of all findings, separated into their respective criticality, potentially with links to their corresponding PoCs.

A Forked TestSuite repo will be helpful for collaborating on tests and PoCs.

Finally, you'll want to take time to set up any infrastructure necessary for tools like Echidna, Manticore, Foundry, etc...

{% hint style="info" %}
There will be a complete separate guide on security tooling. For the time being, these tools are considered out of scope for this handbook.
{% endhint %}


# 1⃣ Beginning The Audit

The audit begins...

Examine The Repo

The best place to start is almost always `README.md`. If there is none (or its a default generated `README.md`) then the second thing to do is scan the `.sol` files for block comments that explain technical design/gotchas.

If you read something that you think might yield an attack vector in the documentation, add a comment next to it and come back to re-evaluate it later. Adding specific tags such as `@audit` are helpful for grepping the codebase to be able to find all of your comments.

Next, run the tests and examine the coverage for any gaps. This will give you a good idea of the quality of code you're dealing with and may point you toward some good areas to focus on once you get into the meat of the audit.

{% hint style="info" %}
[solidity-coverage](https://www.npmjs.com/package/solidity-coverage) is a helpful package for tracking the code coverage of a hardhat test suite.
{% endhint %}

### Build A Mental Model

Your mental model of the smart contract system will lay the foundation for the audit. It is critically important that you have a solid high-level understanding of the contracts before examining the nitty gritty details. Context is key when examining each individual line.

Leverage your prior research about the protocol and enumerate the JTBD (Jobs-To-Be-Done) of the system. Does it enable swapping from one token to another? Can a user swap with multiple tokens in the swapRoute? Are users able to provide liquidity?

After you've listed the high-level functionality of what the contracts aim to do, explore the code paths for each of these. How does someone swap? How does someone provide liquidity?

You have sufficiently walked a code path when you are able to describe the functions/contracts a user's tx interacts with without reading the code. If this is a DEX: how does a swap execute?

{% hint style="info" %}
Do not memorize the lines in each function, just have an idea of what each function accomplishes and the overall call path.
{% endhint %}

As you do this initial skim of the contracts, you'll likely notice things that you don't understand at first or that seem off. Add an `@audit` tag with your thoughts and come back to it later when you have more context.

<figure><img src="https://2713856283-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWRuBMx8L1wxnbq5Ahdl2%2Fuploads%2Fgit-blob-75ba3c1ed2d8d2a1cc6b7fe38d7b9f85c1b34b84%2FScreenshot%202023-01-24%20at%206.06.34%20PM.png?alt=media" alt=""><figcaption><p><code>@audit</code> tags in the code</p></figcaption></figure>

{% hint style="info" %}
It is extremely important to make note of everything that seems off, when exploring a large codebase it is easy to become overwhelmed and forget each code smell you had.
{% endhint %}

As you read and have questions, reach out to the other auditors, they may have insights or be wondering the same thing. Combining your mental model with the other auditors drastically speeds up the process of gaining context and saves precious time during the audit.

Sometimes it may be helpful to make a call graph with tools like [Surya](https://github.com/ConsenSys/surya). Call graphs can help visualize the system and give context as you're walking through the code path, and can serve as an aid when communicating with other auditors.

{% hint style="info" %}
When using a call graph, it may be especially useful to mark the contracts that hold vital storage and the contracts that hold funds.
{% endhint %}


# 2⃣ The Meat of The Audit

Digging in

### Pulling Threads

At this point, you understand the contract system at a high level. Now it's time to go back to your `@audit` tags and start pulling on threads.

Examine the execution path for any edge cases you tagged, and do some further digging for the questions you left.

Now is the time to start thinking about all the common attack vectors you have studied. As you pull threads, be on the lookout for all of the common bugs/vulnerabilities enumerated in the [encyclopedias (under construction)](/encyclopedia-of-common-solidity-bugs). In addition to common attack vectors, simple typos often yield critical bugs. Be sure to read each line carefully and look out for typos as you go.

As you see potential attacks or bugs leave an `@audit` tag and come back to them after you finish your current thread. Sometimes it might be helpful to write a PoC to test your potential attack and verify the behavior matches your expectations.

If you pull a thread and you decide it's a viable vulnerability/finding, add it to the findings doc with a precise description referencing exact line numbers. Ideally, other auditors should be able to understand the finding without reaching out for more context. After you add a finding to the doc, post it in the group or make a note to bring it up during a call so other auditors can learn from the finding.

Remember to make `@audit` tags for gas optimizations you notice along the way as well, although these are not a priority.

<figure><img src="https://2713856283-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWRuBMx8L1wxnbq5Ahdl2%2Fuploads%2Fgit-blob-14a7bd054b36bf0afcb69bfcc00cdf396544ebb8%2FScreenshot%202023-01-25%20at%2012.02.27%20AM.png?alt=media" alt=""><figcaption></figcaption></figure>

### Ideating Attack Vectors

After you've pulled all of your initial threads, you'll have a substantial amount of knowledge about the protocol and you'll need to come up with new threads to pull.

Firstly, run a static analyzer like [Slither](https://github.com/crytic/slither) and mark all of the interesting flags in-code with an `@audit` tag.

Many of the bugs and vulnerabilities that affect a codebase are particular to the protocol. Protocol-unique findings simply require context (which we already have) and creativity. Here's how we can hack creativity and come up with interesting bespoke attack vectors:

* Enumerate all of the knobs an attacker can control. What public/external functions are there? What state could they affect? Does it matter if some of these tx's are frontrun? Can sending ERC20 tokens or Ether to an address change the behavior of the contracts?\
  \
  Share this list with your fellow auditors. These are your legos to create attacks with, the more you have the better.\\
* Utilize divergent thinking. Instead of verifying that an invariant holds, list the ways that it might break. Can you achieve any of these with the list of knobs you built?\
  \
  Ex - How can `amount into the vault != amount out of the vault`:
  * Fee-on-transfer tokens
  * The attacker front-runs someone to manipulate the vault token pricing
  * The attacker reenters on withdrawal
  * The attacker sandwich attacks a harvest
  * The attacker abuses some precision loss
  * The list goes on...

Once you've generated a few `@audit` tags and a handful of possible attack vectors, it's time to start pulling those threads again. Auditing is an iterative process of examining code paths, making `@audit` tags, learning more about the code, and coming up with new attack vectors.

<figure><img src="https://2713856283-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWRuBMx8L1wxnbq5Ahdl2%2Fuploads%2Fgit-blob-fe87ae18c12c0066b0f93930b42ffe1f9b52bbac%2FUntitled.png?alt=media" alt=""><figcaption></figcaption></figure>

If you truly run out of ideas/threads to pull on, do a deep review of every file, reading line-by-line and leaving `@audit` tags for things that you don't understand or seem off. This is a guaranteed way to yield more threads to pull on.

{% hint style="info" %}
This is also a good stage to come up with invariants to later verify with security tools that perform fuzzing, symbolic analysis, or formal verification. More on this in "Security Tools" (under construction).
{% endhint %}

### Collaboration

When you stumble across something particularly alarming/confusing in the code, alert your fellow auditors. There's a good chance they noticed it too and have their own thoughts about it.

Discuss attack vectors as a group as much as possible, a lot of times you’ll hop on a call with an idea for an attack and it turns out to be invalid, but your fellow auditor has context on another part of the system that could combine with your idea to yield a valid vulnerability.

Invest heavily in the shared knowledge of your group. The corpus of the group allows you to ideate new attack vectors and validate/invalidate potential leads much faster than any individual auditor.


# 3⃣ Writing Tests & PoCs

Time to test!

### Tests

If the test coverage is poor, fill in the gaps. By writing tests, you get a more intimate understanding of the contracts + there’s a good chance you find a bug (untested code is hearsay). There are some bugs that are much more obvious to a runtime execution than to human manual analysis.

Test suites should aspire to reach 100% code coverage, the behavior of untested code paths is dubious.

While you're writing tests, you may encounter some odd behaviors that you didn’t realize before — `@audit` tag them and explore these further after you finish your current thought/test.

### PoCs

Now take the time to PoC any findings/attack vectors that you haven't already. It’s important to comment throughout each PoC sufficiently, both for your own understanding and others.

While writing a PoC, you might discover that the system does not function as you thought it did, and your attack is not viable. In this case, go back to the drawing board and examine how your attack could be tweaked (the list of knobs is helpful here) so that the attack is valid.

{% hint style="info" %}
Now is a good time to use security tools to verify invariants you identified during the previous stage, etc... ("Security Tools" under construction)
{% endhint %}


# 4⃣ Wrapping Up The Audit

It's been fun

### Tie Up Loose Ends

The audit is coming to an end!

Review and resolve all `@audit` tags and make sure every last note/thought you had is resolved. Then take the time to double-check that all of your findings are in the doc and that they are adequately documented with accurate line numbers and suggested changes.

### Validate Findings

After everyone has finished their review, go through the findings doc and independently validate each finding. More complex/dubious findings can be discussed and defended as a group.

As a part of the verification process, ensure that the recommended fix fully resolves the issue in a desirable way.

{% hint style="info" %}
PoCs are critical for sharing and defending a finding, make sure your PoCs are legible and sufficiently commented.
{% endhint %}

### Create The Report

Now it's time to create the report!

For each valid finding, populate a slide in the report with the description, file/line, criticality, status, and remediation. Read through the description/remediation several times to correct any typos and re-word as necessary.

For high-severity findings, link to their corresponding PoCs for more context.

Deliver the report along with the test suite repo. With the repo included, the team can refer to PoCs, see the due diligence that was done, and perhaps even adopt the test suite as they make remediations.


# 5⃣ After The Audit

It's not over!

### Continued Review

After delivering the report, make yourself available for any clarifications/advice on resolutions.

Be sure to review the amendments made to each finding and validate that they fully resolve the original issue and do not create new ones.

If the amendments are significant they may constitute a new audit. Contracts may need to go through several stages of audits until they are ready for deployment.


# 6⃣ Addendum

You are now equipped with an organized process to perform quality smart contract audits. Group up with like-minded individuals, follow this process, and deliver superb audits to do your share and secure the bleeding edge of blockchain.


