Core Contracts

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

Multiple Transfer Event Emissions in DebtToken Contract

Summary

The DebtToken contract emits the Transfer event multiple times for the same operation - once in _update(), once explicitly in mint(), and once through the parent ERC20 implementation. This redundancy can cause confusion for off-chain applications and indexers tracking token transfers.

Vulnerability Details

The Transfer event is emitted in three locations:

  • in _update():

function _update(address from, address to, uint256 amount) internal virtual override {
//..First emission
emit Transfer(from, to, amount);
}
  • In mint():

function mint(
address user,
address onBehalfOf,
uint256 amount,
uint256 index
) external override onlyReservePool returns (bool, uint256, uint256) {
_mint(onBehalfOf, amountToMint.toUint128());
//..Second emission
emit Transfer(address(0), onBehalfOf, amountToMint);
}
  • Through the parent ERC20 implementation when calling _mint() which calls _update() which calls super._update() :

function _update(address from, address to, uint256 value) internal virtual {
//.. Third emission in ERC20 implementation
emit Transfer(from, to, value);
}

PoC

In order to run the test you need to:

  1. Run foundryup to get the latest version of Foundry

  2. Install hardhat-foundry: npm install --save-dev @nomicfoundation/hardhat-foundry

  3. Import it in your Hardhat config: require("@nomicfoundation/hardhat-foundry");

  4. Make sure you've set the BASE_RPC_URL in the .env file or comment out the forking option in the hardhat config.

  5. Run npx hardhat init-foundry

  6. There is one file in the test folder that will throw an error during compilation so rename the file in test/unit/libraries/ReserveLibraryMock.sol to => ReserveLibraryMock.sol_broken so it doesn't get compiled anymore (we don't need it anyways).

  7. Create a new folder test/foundry

  8. Paste the below code into a new test file i.e.: FoundryTest.t.sol

  9. Run the test: forge test --mc FoundryTest -vvvv

If you look at your terminal you can see that three Transfer events get emitted:

emit Transfer(from: 0x0000000000000000000000000000000000000000, to: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], value: 100000000000000000000 [1e20])
emit Transfer(from: 0x0000000000000000000000000000000000000000, to: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], value: 100000000000000000000 [1e20])
emit Transfer(from: 0x0000000000000000000000000000000000000000, to: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], value: 100000000000000000000 [1e20])
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {console2} from "forge-std/console2.sol";
import {DebtToken} from "../../contracts/core/tokens/DebtToken.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {crvUSDToken} from "../../contracts/mocks/core/tokens/crvUSDToken.sol";
import {WadRayMath} from "../../contracts/libraries/math/WadRayMath.sol";
contract MockLendingPool {
using WadRayMath for uint256;
uint256 private _normalizedDebt = WadRayMath.RAY;
function getNormalizedDebt() external view returns (uint256) {
return _normalizedDebt;
}
function setNormalizedDebt(uint256 newValue) external {
_normalizedDebt = newValue;
}
}
contract FoundryTest is Test {
using WadRayMath for uint256;
DebtToken public debtToken;
MockLendingPool public lendingPool;
crvUSDToken public crvusd;
address public owner;
address public user1;
address public user2;
// Events for testing
event Transfer(address indexed from, address indexed to, uint256 value);
event Mint(address indexed caller, address indexed onBehalfOf, uint256 amount, uint256 index);
event Burn(address indexed from, address indexed receiverOfUnderlying, uint256 amount, uint256 index);
function setUp() public {
// Setup accounts
owner = address(this);
user1 = makeAddr("user1");
user2 = makeAddr("user2");
crvusd = new crvUSDToken(owner);
crvusd.setMinter(owner);
// Deploy mock pool
lendingPool = new MockLendingPool();
// Deploy tokens
debtToken = new DebtToken("DebtToken", "DT", owner);
// Set the pool for RToken
debtToken.setReservePool(address(lendingPool));
}
function test_multipleTransferEventEmitted() public {
uint256 amount = 100e18;
uint256 index = lendingPool.getNormalizedDebt();
vm.startPrank(address(lendingPool));
debtToken.mint(user1, user1, amount, index);
vm.stopPrank();
}
}

Impact

There is no direct impact on the protocol but on off-chain applications like blockchain explorers etc.. this leads to:

  • Confusion for off-chain applications tracking token transfers

  • Increased gas costs due to redundant event emissions

  • Potential issues with protocols integrating with the DebtToken that rely on Transfer events

Tools Used

  • Foundry

  • Terminal

  • Manual Review

Recommendations

Remove redundant Transfer event emissions in DebtToken::_update() and mint() and standardize on a single emission point. The recommended approach is to rely on the ERC20 implementation's event emission.

Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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