We do not have a reliable USD price for the recorded assets yet.
0x6598dad18bda89a0e58a1f427c8cebc0de90f153EthereumdTRINITY’s USDC reserve was exploitable through a two-transaction ACT sequence. In tx 0x8d33d688def03551cb77b0463f55ae5a670f5ebf3bbb5b8aa0e284c040ae7139, the attacker used public supply, withdraw, and flashloan entrypoints on L2Pool 0x6598dad18bda89a0e58a1f427c8cebc0de90f153 to leave the reserve at dust liquidity and then compound the reserve liquidityIndex through 150 self-flashloans. In tx 0xbec4c8ae19c44990984fd41dc7dd1c9a22894adccf31ca6b61b5aa084fc33260, the attacker consumed that manipulated index to turn repeated small USDC deposits into larger USDC withdrawals and then borrowed dStable. The root cause is the combination of attacker-amplifiable index compounding and scaled-balance rounding that broke the invariant that collateral value and borrow power must remain bounded by economically supplied capital.
dTRINITY’s victim pool is the L2Pool proxy 0x6598dad18bda89a0e58a1f427c8cebc0de90f153, backed by verified implementation 0xfda3a0effe2f3917aa60e0741c6788619ae19e84. The exploited reserve asset was USDC 0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf, with aToken 0x504d0eacbf9ea5645a8a9da1b15f3708a5483acc. The profit leg used dStable 0x07fff99e1664d9b116fbc158c0e99785f81ca236, whose reserve holder was 0x5cc741931d01cb1adde193222dfb1ad75930fd60.
The attacker cluster consisted of EOA 0x08cfdff8ded5f1326628077f38d4f90df6417fd9, orchestrator 0xba5e1e36b0305772d35509c694782fb9118d4ecc, and helper . The orchestrator was deployed in tx , then used to execute the exploit transactions.
0x149ad6f80e14be0f591c02a8f476fea160ffcea70xd31e688ee1b14b98ab72e0746f6deafdf6178bcae663e51014d73e43623c0dcdThe exploit depended on two protocol properties working together in an unsafe way. First, flashloan premiums were added into the reserve liquidity index through ReserveLogic::cumulateToLiquidityIndex, and that calculation used the current reserve liquidity as the denominator. Because any user could push the USDC reserve to dust liquidity first, the same public flashloan premium path could then multiply the index aggressively.
Second, the manipulated index immediately fed into the aToken scaled-balance accounting path. Both _mintScaled and _burnScaled convert between underlying and scaled units with amount.rayDiv(index), while SupplyLogic::executeWithdraw values a user’s claim with scaledBalanceOf(user).rayMul(nextLiquidityIndex). Under an attacker-controlled extreme index, those conversions quantized to tiny scaled balances that could still be revalued into larger underlying withdrawals. The result was excess USDC claim and collateral value that did not correspond to legitimate supplied principal or yield.
The core code path is explicit in the collected victim source:
function cumulateToLiquidityIndex(
DataTypes.ReserveData storage reserve,
uint256 totalLiquidity,
uint256 amount
) internal returns (uint256) {
uint256 result = (amount.wadToRay().rayDiv(totalLiquidity.wadToRay()) + WadRayMath.RAY).rayMul(
reserve.liquidityIndex
);
reserve.liquidityIndex = result.toUint128();
return result;
}
FlashLoanLogic::_handleFlashLoanRepayment calls that function on every flashloan repayment:
reserveCache.nextLiquidityIndex = reserve.cumulateToLiquidityIndex(
IERC20(reserveCache.aTokenAddress).totalSupply() +
uint256(reserve.accruedToTreasury).rayMul(reserveCache.nextLiquidityIndex),
premiumToLP
);
In tx 0x8d33d688def03551cb77b0463f55ae5a670f5ebf3bbb5b8aa0e284c040ae7139, the attacker first supplied 100 raw USDC units and withdrew 99, leaving only dust aToken liquidity in the reserve. The collected trace then shows the USDC reserve ending at the manipulated index 6226621999999999999999999979728276 after the flashloan loop:
emit ReserveDataUpdated(
reserve: USDC,
liquidityIndex: 6226621999999999999999999979728276
)
Once that state existed, the seed transaction reused it through the scaled-balance accounting path:
function _mintScaled(address caller, address onBehalfOf, uint256 amount, uint256 index) internal returns (bool) {
uint256 amountScaled = amount.rayDiv(index);
require(amountScaled != 0, Errors.INVALID_MINT_AMOUNT);
...
}
function _burnScaled(address user, address target, uint256 amount, uint256 index) internal {
uint256 amountScaled = amount.rayDiv(index);
require(amountScaled != 0, Errors.INVALID_BURN_AMOUNT);
...
}
SupplyLogic::executeWithdraw later values the claim using the manipulated index:
uint256 userBalance = IAToken(reserveCache.aTokenAddress).scaledBalanceOf(msg.sender).rayMul(
reserveCache.nextLiquidityIndex
);
The seed trace shows the helper repeatedly minting and burning against that index. One representative cycle is:
L2Pool::deposit(USDC, 3175576, 0x149a...cea7, 0)
emit Mint(... value: 3175576, index: 6226621999999999999999999979728276)
...
L2Pool::withdraw(USDC, 9339930, 0x149a...cea7)
emit Burn(... value: 9339930, index: 6226621999999999999999999979728276)
emit Withdraw(... amount: 9339930)
That is the protocol invariant break. A small fresh deposit created a scaled position that, when revalued with the attacker-controlled index, supported a much larger withdrawal. The attacker then used the inflated collateral state to borrow dStable from the protocol.
The exploit flow is fully permissionless. The attacker EOA deployed orchestrator 0xba5e1e36b0305772d35509c694782fb9118d4ecc, then used that contract to interact only with public dTRINITY functions.
In the first exploit transaction, the orchestrator:
100 raw USDC and withdrew 99 to leave dust liquidity.cumulateToLiquidityIndex updates against a dust denominator.In the second exploit transaction, the orchestrator:
257324747681921149441808 raw dStable units.0x149ad6f80e14be0f591c02a8f476fea160ffcea7.12453242 raw USDC units.3175576 deposit / 9339930 withdraw cycle on-chain.The seed receipt confirms the final profit transfer from the dStable reserve holder to the attacker cluster:
Transfer(
from: 0x5cc741931d01cb1adde193222dfb1ad75930fd60,
to: 0xba5e1e36b0305772d35509c694782fb9118d4ecc,
value: 257324747681921149441808
)
The exploit broke dTRINITY’s collateral accounting on the USDC reserve. It allowed the attacker to extract more USDC than the fresh helper deposits justified, keep manipulated aUSDC collateral, and borrow dStable against that synthetic collateral value. The measured loss captured in the root cause artifact is:
dStable: 257324747681921149441808 raw units (18 decimals)The affected protocol components were the L2Pool reserve accounting path, the USDC aToken scaled-balance tokenization path, and the dStable reserve exposed to the inflated borrow power.
0x8d33d688def03551cb77b0463f55ae5a670f5ebf3bbb5b8aa0e284c040ae71390xbec4c8ae19c44990984fd41dc7dd1c9a22894adccf31ca6b61b5aa084fc332600xd31e688ee1b14b98ab72e0746f6deafdf6178bcae663e51014d73e43623c0dcd0x6598dad18bda89a0e58a1f427c8cebc0de90f1530xfda3a0effe2f3917aa60e0741c6788619ae19e840xcbb7c0000ab88b473b1f5afd9ef808440eed33bf0x504d0eacbf9ea5645a8a9da1b15f3708a5483acc0x07fff99e1664d9b116fbc158c0e99785f81ca2360xba5e1e36b0305772d35509c694782fb9118d4ecc0x149ad6f80e14be0f591c02a8f476fea160ffcea7