Core Contracts

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

Global Total Value Update Ignores Token Specificity, Leading to Inaccurate Accounting

Summary

The deposit function in the `Treasury.sol` updates a global variable _totalValue with the deposited amount regardless of the token type. This approach aggregates deposits from different tokens into a single total, potentially causing miscalculations in the overall system value and misallocation of funds.

Vulnerability Details

Within the `deposit()` in the `Treasury.sol`, after processing the deposit, the code updates the global total:
```solidity
_totalValue += amount;
```
This update does not account for the fact that different tokens may have different values or decimal representations. Aggregating all deposits into a single `_totalValue` can be misleading when the tokens involved are not homogeneous in value.
### Proof of Concept
Consider the following scenario:
User A deposits 100 units of Token X (high value).
User B deposits 100 units of Token Y (low value).
Both deposits are summed into `_totalValue` without differentiation. Subsequent calculations that rely on `_totalValue` will treat both tokens equally, potentially causing errors in value-based distributions.
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../../../contracts/core/collectors/Treasury.sol";
import "lib/openzeppelin-contracts/contracts/mocks/token/ERC20Mock.sol";
contract MockERC20 is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
_mint(msg.sender, 100_000 * 10 ** 18); // Mint 100k tokens to deployer
}
}
contract TreasuryTest is Test {
Treasury treasury;
MockERC20 token;
address admin = address(0x1);
address manager = address(0x2);
address allocator = address(0x3);
address user = address(0x7);
address userA = address(0x4);
address userB = address(0x6);
address recipient = address(0x5);
// Role hashes
bytes32 constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
bytes32 constant ALLOCATOR_ROLE = keccak256("ALLOCATOR_ROLE");
function setUp() public {
// Deploy mock token
token = new MockERC20("Mock Token", "MTK");
// Deploy Treasury contract with admin
vm.prank(admin);
treasury = new Treasury(admin);
// Grant roles
vm.startPrank(admin);
treasury.grantRole(MANAGER_ROLE, manager);
treasury.grantRole(ALLOCATOR_ROLE, allocator);
vm.stopPrank();
// Transfer some tokens to user for testing deposits
token.transfer(userA, 10_000 * 10 ** 18);
token.transfer(userB, 10_000 * 10 ** 18);
}
function testTotalValueMisrepresentsTokenValue() public {
// Deploy two tokens with same decimals but implied different values
MockERC20 tokenX = new MockERC20("Token X", "TKX"); // High-value token, e.g., $100/unit
MockERC20 tokenY = new MockERC20("Token Y", "TKY"); // Low-value token, e.g., $0.01/unit
// Transfer tokens to users
tokenX.transfer(userA, 1_000 * 10**18); // 1000 units of Token X
tokenY.transfer(userB, 1_000 * 10**18); // 1000 units of Token Y
// User A deposits 100 units of Token X
uint256 depositAmountX = 100 * 10**18; // 100 Token X
vm.startPrank(userA);
tokenX.approve(address(treasury), depositAmountX);
treasury.deposit(address(tokenX), depositAmountX);
vm.stopPrank();
// User B deposits 100 units of Token Y
uint256 depositAmountY = 100 * 10**18; // 100 Token Y
vm.startPrank(userB);
tokenY.approve(address(treasury), depositAmountY);
treasury.deposit(address(tokenY), depositAmountY);
vm.stopPrank();
// Check individual balances
assertEq(treasury.getBalance(address(tokenX)), depositAmountX, "Token X balance incorrect");
assertEq(treasury.getBalance(address(tokenY)), depositAmountY, "Token Y balance incorrect");
// Check totalValue
uint256 actualTotalValue = treasury.getTotalValue();
uint256 rawSum = depositAmountX + depositAmountY; // 100 * 10^18 + 100 * 10^18 = 200 * 10^18
// Log values for inspection
console.log("Token X Deposited (high value):", depositAmountX / 10**18, "units");
console.log("Token Y Deposited (low value):", depositAmountY / 10**18, "units");
console.log("Raw Total Value:", actualTotalValue / 10**18, "units");
// Assertion matches current logic but highlights the issue
assertEq(actualTotalValue, rawSum, "Total value matches raw sum but misrepresents real value");
// Commentary: In reality, if Token X = $100/unit and Token Y = $0.01/unit:
// - 100 Token X = $10,000
// - 100 Token Y = $1
// - Real total value = $10,001, but _totalValue = 200 units, which is misleading
console.log("Note: Total Value treats 100 high-value Token X and 100 low-value Token Y as equal (200 units)");
}
}
```
the result
```solidity
Ran 1 test for test/foundry/Collectors/TreasuryTest.t.sol:TreasuryTest
[PASS] testTotalValueMisrepresentsTokenValue() (gas: 1905432)
Logs:
Token X Deposited (high value): 100 units
Token Y Deposited (low value): 100 units
Raw Total Value: 200 units
Note: Total Value treats 100 high-value Token X and 100 low-value Token Y as equal (200 units)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.00ms (742.13µs CPU time)
Ran 1 test suite in 11.38ms (2.00ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```

Impact

Inaccurate Accounting: Combining deposits from tokens with different valuations can lead to incorrect computations of the system’s total value.
Misallocation of Resources: Future operations relying on _totalValue (e.g., fee calculations, reward distributions) may be skewed due to the mixed valuation of tokens.
Potential Exploitation: An attacker might exploit these miscalculations to manipulate fee or reward distributions, compromising the protocol’s financial integrity.

Tools Used

Manual Review and Foundry

Recommendations

Token-Specific Total Accounting:
Instead of using a single global _totalValue, maintain a per-token total with a mapping:
```solidity
mapping(address => uint256) private _tokenTotalValue;
```
Refactor the Deposit Function:
Update the function to account for each token individually:
```solidity
function deposit(address token, uint256 amount) external override nonReentrant {
if (token == address(0)) revert InvalidAddress();
if (amount == 0) revert InvalidAmount();
IERC20(token).transferFrom(msg.sender, address(this), amount);
_balances[token] += amount;
_tokenTotalValue[token] += amount;
emit Deposited(token, amount);
}
```
This change ensures that each token's total value is tracked separately, leading to more accurate accounting and reducing the risk of miscalculations in further financial operations.
Updates

Lead Judging Commences

inallhonesty Lead Judge 6 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.