Skip to content
This repository has been archived by the owner on Jul 21, 2024. It is now read-only.

0xpep7 - CouncilMember:burn renders the contract inoperable after the first execution #199

Open
sherlock-admin2 opened this issue Jan 15, 2024 · 5 comments
Assignees
Labels
Has Duplicates A valid issue with 1+ other issues describing the same vulnerability High A valid High severity issue Reward A payout will be made for this issue Sponsor Confirmed The sponsor acknowledged this issue is valid Will Fix The sponsor confirmed this issue will be fixed

Comments

@sherlock-admin2
Copy link
Contributor

sherlock-admin2 commented Jan 15, 2024

0xpep7

high

CouncilMember:burn renders the contract inoperable after the first execution

Summary

The CouncilMember contract suffers from a critical vulnerability that misaligns the balances array after a successful burn, rendering the contract inoperable.

Vulnerability Detail

The root cause of the vulnerability is that the burn function incorrectly manages the balances array, shortening it by one each time an ERC721 token is burned while the latest minted NFT still withholds its unique tokenId which maps to the previous value of balances.length.

// File: telcoin-audit/contracts/sablier/core/CouncilMember.sol
210:    function burn(
        ...
220:        balances.pop(); // <= FOUND: balances.length decreases, while latest minted nft withold its unique tokenId
221:        _burn(tokenId);
222:    }

This misalignment between existing tokenIds and the balances array results in several critical impacts:

  1. Holders with tokenId greater than the length of balances cannot claim.
  2. Subsequent burns of tokenId greater than balances length will revert.
  3. Subsequent mint operations will revert due to tokenId collision. As totalSupply now collides with the existing tokenId.
// File: telcoin-audit/contracts/sablier/core/CouncilMember.sol
173:    function mint(
        ...
179:
180:        balances.push(0);
181:        _mint(newMember, totalSupply());// <= FOUND
182:    }

This mismanagement creates a cascading effect, collectively rendering the contract inoperable. Following POC will demonstrate the issue more clearly in codes.

POC

Run git apply on the following patch then run npx hardhat test to run the POC.

diff --git a/telcoin-audit/test/sablier/CouncilMember.test.ts b/telcoin-audit/test/sablier/CouncilMember.test.ts
index 675b89d..ab96b08 100644
--- a/telcoin-audit/test/sablier/CouncilMember.test.ts
+++ b/telcoin-audit/test/sablier/CouncilMember.test.ts
@@ -1,13 +1,14 @@
 import { expect } from "chai";
 import { ethers } from "hardhat";
 import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
-import { CouncilMember, TestTelcoin, TestStream } from "../../typechain-types";
+import { CouncilMember, TestTelcoin, TestStream, ERC721Upgradeable__factory } from "../../typechain-types";
 
 describe("CouncilMember", () => {
     let admin: SignerWithAddress;
     let support: SignerWithAddress;
     let member: SignerWithAddress;
     let holder: SignerWithAddress;
+    let lastCouncilMember: SignerWithAddress;
     let councilMember: CouncilMember;
     let telcoin: TestTelcoin;
     let stream: TestStream;
@@ -18,7 +19,7 @@ describe("CouncilMember", () => {
     let supportRole: string = ethers.keccak256(ethers.toUtf8Bytes("SUPPORT_ROLE"));
 
     beforeEach(async () => {
-        [admin, support, member, holder, target] = await ethers.getSigners();
+        [admin, support, member, holder, target, lastCouncilMember] = await ethers.getSigners();
 
         const TestTelcoinFactory = await ethers.getContractFactory("TestTelcoin", admin);
         telcoin = await TestTelcoinFactory.deploy(admin.address);
@@ -182,6 +183,22 @@ describe("CouncilMember", () => {
                 it("the correct removal is made", async () => {
                     await expect(councilMember.burn(1, support.address)).emit(councilMember, "Transfer");
                 });
+                it.only("inoperable contract after burn", async () => {
+                    await expect(councilMember.mint(lastCouncilMember.address)).to.not.reverted;
+
+                    // This 1st burn will cause contract inoperable due to tokenId & balances misalignment
+                    await expect(councilMember.burn(1, support.address)).emit(councilMember, "Transfer");
+
+                    // Impact 1. holder with tokenId > balances length cannot claim
+                    await expect(councilMember.connect(lastCouncilMember).claim(3, 1)).to.revertedWithPanic("0x32"); // @audit-info 0x32: Array accessed at an out-of-bounds or negative index
+
+                    // Impact 2. subsequent burns of tokenId > balances length will revert
+                    await expect(councilMember.burn(3, lastCouncilMember.address)).to.revertedWithPanic("0x32"); 
+
+                    // Impact 3. subsequent mint will revert due to tokenId collision
+                    await expect(councilMember.mint(lastCouncilMember.address)).to.revertedWithCustomError(councilMember, "ERC721InvalidSender");
+
+                });
             });
         });
 

Result

CouncilMember
mutative
burn
Success
✔ inoperable contract after burn (90ms)
1 passing (888ms)

The Passing execution of the POC confirmed that operations such as claim, burn & mint were all reverted which make the contract inoperable.

Impact

The severity of the vulnerability is high due to the high likelihood of occurence and the critical impacts on the contract's operability and token holders' ability to interact with their assets.

Code Snippet

https://github.com/sherlock-audit/2024-01-telcoin/blob/main/telcoin-audit/contracts/sablier/core/CouncilMember.sol#L220

Tool used

VsCode

Recommendation

It is recommended to avoid popping out balances to keep alignment with uniquely minted tokenId. Alternatively, consider migrating to ERC1155, which inherently manages a built-in balance for each NFT.

@github-actions github-actions bot added High A valid High severity issue Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label labels Jan 19, 2024
@sherlock-admin2
Copy link
Contributor Author

1 comment(s) were left on this issue during the judging contest.

takarez commented:

valid because { this is a valid findings because the watson explain how again the burn function will break a functionality just like the previous issue thus making it a dupp of 109}

@nevillehuang nevillehuang reopened this Jan 25, 2024
@nevillehuang nevillehuang added Has Duplicates A valid issue with 1+ other issues describing the same vulnerability and removed Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label labels Jan 25, 2024
This was referenced Jan 25, 2024
This was referenced Jan 25, 2024
@nevillehuang
Copy link
Collaborator

See comments here for duplication reasons.

@sherlock-admin sherlock-admin changed the title Happy Yellow Wolf - CouncilMember:burn renders the contract inoperable after the first execution 0xpep7 - CouncilMember:burn renders the contract inoperable after the first execution Jan 29, 2024
@sherlock-admin sherlock-admin added the Reward A payout will be made for this issue label Jan 29, 2024
@amshirif amshirif self-assigned this Jan 30, 2024
@amshirif amshirif added the Will Fix The sponsor confirmed this issue will be fixed label Jan 30, 2024
@amshirif
Copy link

@sherlock-admin sherlock-admin added the Sponsor Confirmed The sponsor acknowledged this issue is valid label Feb 7, 2024
@sherlock-admin
Copy link
Contributor

The protocol team fixed this issue in PR/commit https://github.com/telcoin/telcoin-audit/pull/31.

@sherlock-admin
Copy link
Contributor

The Lead Senior Watson signed off on the fix.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Has Duplicates A valid issue with 1+ other issues describing the same vulnerability High A valid High severity issue Reward A payout will be made for this issue Sponsor Confirmed The sponsor acknowledged this issue is valid Will Fix The sponsor confirmed this issue will be fixed
Projects
None yet
Development

No branches or pull requests

4 participants