Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: medium
Valid

The Treasury reverts all deposits after an attacker deposits a worthless token

Summary

The Treasury allows any tokens to be deposited by anyone. If a huge number of worthless tokens are deposited, all other deposits will fail.

Finding Description

In Treasury.sol, the deposit function accepts any token. When it is deposited, it sums the total number of units deposited across all token types:

function deposit(address token, uint256 amount) external override nonReentrant {
...
_totalValue += amount;
...
}

Source: Treasury.sol#L52

Without any sort of allow list for which tokens may be used here, this effectively allows anyone to use worthless or fake ERC20 contracts in order to submit any amount they like. Once the _totalValue is at or close to uint256.max, all future attempts to deposit will revert due to an overflow.

Impact Explanation

If someone deposits worthless tokens into the Treasury, the Treasury is effectively useless.

An attacker can call deposit with their worthless tokens anytime in order to block real deposits from occurring. This could be periodic or as a frontrun for incoming deposits.

Withdraws still work so funds are not trapped, we just cannot deposit again. If the manager withdraws the worthless tokens, deposits will function again.. until the attack is repeated.

Likelihood Explanation

This is a 0 cost way to grief the system (other than a little gas costs). Once exposed, someone may do this just to disrupt the protocol.

Proof of Concept

The following git patch updates the Treasury test file to show that any token may be deposited, allowing a huge number of worthless tokens to be deposited. Then any real attempts to deposit will revert. Run with npm run test:unit:collectors.

diff --git a/test/unit/core/collectors/Treasury.test.js b/test/unit/core/collectors/Treasury.test.js
index 7f27080..eb0ffc2 100644
--- a/test/unit/core/collectors/Treasury.test.js
+++ b/test/unit/core/collectors/Treasury.test.js
@@ -1,6 +1,7 @@
import { expect } from "chai";
import hre from "hardhat";
const { ethers } = hre;
+import { PANIC_CODES } from "@nomicfoundation/hardhat-chai-matchers/panic.js";
describe("Treasury", () => {
let treasury;
@@ -32,6 +33,29 @@ describe("Treasury", () => {
await treasury.grantRole(ALLOCATOR_ROLE, allocator.address);
});
+ it.only("Bug", async () => {
+ const legitUser = user1;
+ const attacker = user2;
+
+ // Create a ERC20 with uncapped mints:
+ let worthlessScamCoin;
+ {
+ const MockToken = await ethers.getContractFactory("MockToken");
+ worthlessScamCoin = await MockToken.deploy("Worthless Scam", "SCAM", 18);
+ await worthlessScamCoin.mint(attacker.address, ethers.MaxUint256);
+ }
+
+ // Deposit a huge number of worthless scam tokens into the treasury:
+ await worthlessScamCoin.connect(attacker).approve(treasury.target, ethers.MaxUint256);
+ await treasury.connect(attacker).deposit(worthlessScamCoin.target, ethers.MaxUint256);
+
+ // BUG: Now real tokens cannot be deposited due to an overflow:
+ await token.connect(legitUser).approve(treasury.target, ethers.MaxUint256);
+ await expect(
+ treasury.connect(legitUser).deposit(token.target, ethers.parseEther("1"))
+ ).to.be.revertedWithPanic(PANIC_CODES.ARITHMETIC_OVERFLOW)
+ })
+
describe("Access Control", () => {
it("should set correct roles on deployment", async () => {
expect(await treasury.hasRole(MANAGER_ROLE, manager.address)).to.be.true;

Recommendation

Remove getTotalValue() and uint256 _totalValue from this contract. Different tokens have wildly different conversion rates, so _totalValue does not report anything useful in it's current state.

Here is the git patch updating ITreasury and Treasury:

When applied, the POC test above fails (since the it no longer reverts).

diff --git a/contracts/core/collectors/Treasury.sol b/contracts/core/collectors/Treasury.sol
index fc8cad6..3985937 100644
--- a/contracts/core/collectors/Treasury.sol
+++ b/contracts/core/collectors/Treasury.sol
@@ -24,7 +24,6 @@ contract Treasury is ITreasury, AccessControl, ReentrancyGuard {
// State variables
mapping(address => uint256) private _balances; // Token balances
mapping(address => mapping(address => uint256)) private _allocations; // Allocator => recipient => amount
- uint256 private _totalValue; // Total value across all tokens
/**
* @notice Initializes the Treasury contract with admin roles
@@ -49,7 +48,6 @@ contract Treasury is ITreasury, AccessControl, ReentrancyGuard {
IERC20(token).transferFrom(msg.sender, address(this), amount);
_balances[token] += amount;
- _totalValue += amount;
emit Deposited(token, amount);
}
@@ -71,7 +69,6 @@ contract Treasury is ITreasury, AccessControl, ReentrancyGuard {
if (_balances[token] < amount) revert InsufficientBalance();
_balances[token] -= amount;
- _totalValue -= amount;
IERC20(token).transfer(recipient, amount);
emit Withdrawn(token, amount, recipient);
@@ -95,14 +92,6 @@ contract Treasury is ITreasury, AccessControl, ReentrancyGuard {
emit FundsAllocated(recipient, amount);
}
- /**
- * @notice Gets total value held by treasury
- * @return Total value across all tokens
- */
- function getTotalValue() external view override returns (uint256) {
- return _totalValue;
- }
-
/**
* @notice Gets balance of specific token
* @param token Address of token to check
diff --git a/contracts/interfaces/core/collectors/ITreasury.sol b/contracts/interfaces/core/collectors/ITreasury.sol
index 107192a..62a200e 100644
--- a/contracts/interfaces/core/collectors/ITreasury.sol
+++ b/contracts/interfaces/core/collectors/ITreasury.sol
@@ -40,12 +40,6 @@ interface ITreasury {
* @notice View functions
*/
- /**
- * @notice Gets total value held by treasury
- * @return Total value across all tokens
- */
- function getTotalValue() external view returns (uint256);
-
/**
* @notice Gets balance of specific token
* @param token Address of token to check

Alternatively getTotalValue could be made useful, for example if an oracle was used to approximate the USD value of each deposit. Then _totalValue could represent the approx total USD value in escrow. But tracking like this may be better done by an off-chain backend watching Deposit and Withdrawn events.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Treasury::deposit increments _totalValue regardless of the token, be it malicious, different decimals, FoT etc.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.