ENF Redeem Decimal Mis-Scaling
Exploit Transactions
Victim Addresses
0xbdb515028a6fa6cd1634b5a9651184494abfd336Ethereum0xf491afe5101b2ee8abc1272fa8e2f85d68828396EthereumLoss Breakdown
Similar Incidents
Pool16 lend/redeem accounting bug drains USDC without HOME backing
34%SilicaPools decimal-manipulation bug drains WBTC flashloan collateral
33%Nowswap V1 WETH Mis-Scaled KLOSS Invariant Exploit
32%Dexible selfSwap allowance drain
30%V3Utils Arbitrary Call Drain
30%Aave AMM LP Oracle Manipulation Through Delegated Recursive LP Looping
29%Root Cause Analysis
ENF Redeem Decimal Mis-Scaling
1. Incident Overview TL;DR
At Ethereum block 16696240, transaction 0x1fe5a53405d00ce2f3e15b214c7486c69cbc5bf165cf9596e86f797f62e81914 called EFVault.redeem(676562, 0x8b5a8333ec272c9bca1e43f4d009e9b2fad5efc9) on 0xbdb515028a6fa6cd1634b5a9651184494abfd336 and converted only 676562 ENF share units into 3436919328971 raw USDC for the chosen receiver plus 3440359688 raw USDC for treasury. The same sender repeated the same redeem path in transaction 0x31565843d565ecab7ab65965d180e45a99d4718fa192c2f2221410f65ea03743 at block 16696260, extracting another 1718487176570 raw USDC for the same receiver.
The root cause is a vault-accounting defect, not a controller or token bug. EFVault stored assetDecimal = 5000000000000 on chain even though the underlying asset was USDC with 6 decimals, and redeem() trusted that stored scale through assetsPerShare(). That multiplied redemptions by roughly 5,000,000x instead of preserving pro-rata share ownership. Because onlyAllowed admits ordinary EOAs and multiple non-incident ENF holders existed before the exploit block, the opportunity was permissionless for any eligible ENF holder.
2. Key Background
EFVault is an upgradeable ERC20 share token whose holders redeem underlying assets through redeem(uint256,address). The vault delegates actual asset withdrawal to controller 0xf491afe5101b2ee8abc1272fa8e2f85d68828396, but the amount requested from the controller is computed inside the vault.
Several public state variables were already in the exploitable configuration at block 16696239, immediately before the first successful redeem:
asset() = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 (USDC)
maxDeposit() = 0
maxWithdraw() = 5000000000000
totalSupply() = 6765625454275
controller.totalAssets(false) = 6880910594692
storage[204] = 5000000000000
balanceOf(0xc3fd2bcb524af31963b3e3bb670f28ba14718244) = 98593356
eth_call redeem(676562, 0x1000000000000000000000000000000000000001) = 3440452523738
Those values matter because a share vault is only safe when redeeming shares returns shares * totalAssets / totalSupply in the underlying token, up to normal rounding and unwind slippage. EFVault instead relied on a stored assetDecimal value that should have matched the underlying token scale but did not.
The call gate was also public to EOAs:
modifier onlyAllowed() {
require(tx.origin == msg.sender || IWhitelist(whiteList).listed(msg.sender), "NON_LISTED_CA");
_;
}
That means the exploit did not require a privileged contract allowlist. It only required an EOA that already held redeemable ENF shares.
ENF shares were transferable before the exploit window, and multiple non-incident holder addresses existed on chain. The exposure was therefore not unique to the observed sender; the relevant prerequisite was possession of ENF shares, not any special relationship with the protocol.
3. Vulnerability Analysis & Root Cause Summary
The vulnerability is an ATTACK-class accounting failure in EFVault. During initialization, the vault stores _assetDecimal directly and never validates it against asset.decimals(). Later, assetsPerShare() multiplies controller.totalAssets(false) by that stored value and by 1e18, and redeem() uses the result to determine the withdrawal request. Any error in assetDecimal therefore linearly scales every redemption. On chain, EFVault stored 5000000000000 instead of the USDC scale 1000000, so the redeem path over-requested assets by 5,000,000x. The controller unwind only determined how much of that inflated request was satisfied from available liquidity; it was not the source of the accounting error.
This breaks three basic security principles. First, critical accounting scales must be validated against canonical token metadata before being stored or used. Second, vault share accounting must preserve proportional ownership under all configured states. Third, initialization of economic parameters must enforce invariant checks instead of trusting arbitrary input values.
4. Detailed Root Cause Analysis
The vulnerable code path is explicit in the collected verified EFVault source:
function initialize(
ERC20Upgradeable _asset,
string memory _name,
string memory _symbol,
uint256 _assetDecimal,
address _whiteList
) public initializer {
__ERC20_init(_name, _symbol);
__Ownable_init();
__ReentrancyGuard_init();
asset = _asset;
assetDecimal = _assetDecimal;
maxDeposit = type(uint256).max;
maxWithdraw = type(uint256).max;
whiteList = _whiteList;
}
function redeem(uint256 shares, address receiver) public returns (uint256 assets) {
require(shares > 0, "ZERO_SHARES");
require(shares <= balanceOf(msg.sender), "EXCEED_TOTAL_BALANCE");
assets = (shares * assetsPerShare()) / 1e24;
require(assets <= maxWithdraw, "EXCEED_ONE_TIME_MAX_WITHDRAW");
_withdraw(assets, shares, receiver);
}
function assetsPerShare() internal view returns (uint256) {
return (IController(controller).totalAssets(false) * assetDecimal * 1e18) / totalSupply();
}
The invariant break is straightforward. Using the publicly readable block-16696239 state, a fair redemption for 676562 share units should have been:
676562 * 6880910594692 / 6765625454275 = 688090 raw USDC
But the deployed formula multiplied that result by assetDecimal / 1e6 = 5000000000000 / 1000000 = 5000000, producing:
676562 * 6880910594692 * 5000000000000 / 6765625454275 / 1000000
= 3440452523738 raw USDC
The independent eth_call from non-incident holder 0xc3fd2bcb524af31963b3e3bb670f28ba14718244 returned exactly that inflated amount at block 16696239. One block later, the mined exploit transaction requested 3440452542877 raw USDC, which is the same failure mode with a small amount of state drift between the pre-state call and the live mined transaction.
The seed trace shows the exact exploit breakpoint:
0xBDB515028A6fA6CD1634B5A9651184494aBfD336::redeem(676562, 0x8B5A8333eC272c9Bca1E43F4d009E9B2FAd5EFc9)
0xf491AfE5101b2eE8abC1272FA8E2f85d68828396::withdraw(3440452542877, 0x8B5A8333eC272c9Bca1E43F4d009E9B2FAd5EFc9)
emit Withdraw(
asset: USDC,
caller: 0xA0959536560776Ef8627Da14c6E8C91E2c743A0a,
owner: 0x8B5A8333eC272c9Bca1E43F4d009E9B2FAd5EFc9,
assets: 3440452542877,
shares: 676562,
fee: 3440359688
)
The receipt logs and balance diff then confirm the concrete economic effect:
USDC Transfer controller -> treasury = 3440359688
USDC Transfer controller -> receiver = 3436919328971
ENF Transfer sender -> burn = 676562
The controller's multi-strategy unwind is therefore downstream execution noise. It explains where the withdrawn USDC came from, but not why the vault asked for millions of USDC in exchange for a tiny number of shares. The code-level defect is EFVault trusting an arbitrary on-chain decimal scale in its redemption math.
The ACT exploit conditions were also concrete and limited: the caller had to control an EOA or otherwise allowed caller with positive ENF balance, the mis-scaled assetDecimal had to remain on chain, and the controller had to have enough live liquidity to satisfy at least part of the inflated withdrawal request.
5. Adversary Flow Analysis
The observed adversary sequence was short and deterministic:
- The sender
0xa0959536560776ef8627da14c6e8c91e2c743a0aalready held ENF before the exploit window. Transaction0x935728aefa3419e5e319ad7b68f22940206526f4d4548408642dbbdbd3015f8cshows148236774share units minted to that address. - The sender then probed redeem sizes with transactions
0xd761f3977b3f0b7c5bd80d0ecd86c12814f52ca4e03d902daee0340878b5c803,0xeaa5d2880e88b6fd71fde8fe2e45d118ec2a51354a3e5ead8505d3cd68f10998,0x645b6007773b7df0a17da16239817656bdb5dc586475a1c083c5571d6b7cbcf1, and0xa58433c7562f2d987b5b8bbc594f4105a9dc0a487576f59777917fa6e25820f4. Several reverted with downstreamTransferHelper: TRANSFER_FAILED, and the later successful transactions show that these probes were used to size the redeem against live controller liquidity. - In the first successful exploit tx
0x1fe5a53405d00ce2f3e15b214c7486c69cbc5bf165cf9596e86f797f62e81914, the sender calledredeem(676562, 0x8b5a8333ec272c9bca1e43f4d009e9b2fad5efc9), burned676562ENF share units, and redirected3436919328971raw USDC to the chosen receiver while treasury collected3440359688raw USDC. - In the second successful exploit tx
0x31565843d565ecab7ab65965d180e45a99d4718fa192c2f2221410f65ea03743, the sender repeated the same676562-share redeem and sent another1718487176570raw USDC to the same receiver while treasury collected1720207383raw USDC. - The path was not unique to the attacker cluster. Before the first exploit tx, non-incident holder
0xc3fd2bcb524af31963b3e3bb670f28ba14718244already held98593356share units, and an independent replay from that holder at block16696239returned the same inflatedredeem(676562, receiver)result.
The calldata-selected receiver is important because the vault lets the caller separate the share-owning sender from the payout recipient. That allowed the observed sender to burn shares from one EOA and route USDC directly to 0x8b5a8333ec272c9bca1e43f4d009e9b2fad5efc9.
6. Impact & Losses
Across the two observed successful transactions, the receiver collected 5155518319106 raw USDC (5,155,518.319106 USDC). Treasury also received 5160567071 raw USDC (5,160.567071 USDC) as protocol fees on the same broken redemption path. Each success consumed only 676562 ENF share units, while a fair block-16696239 redemption for that share amount was 688090 raw USDC.
For the seed transaction alone, the receiver started from 0 USDC and ended at 3436919328971 raw USDC while the sending EOA paid 0.08816796 ETH in gas. The USDC overpayment exceeded the gas spend by several orders of magnitude, so the exploit predicate is a direct profit opportunity rather than a break-even griefing path.
The practical impact is that EFVault's live share pricing was economically broken for every eligible ENF holder while the bad assetDecimal remained in storage and controller liquidity remained available. The loss surface was not limited to the two observed txs; those txs only show the amount actually extracted before conditions changed.
7. References
- Seed exploit tx:
0x1fe5a53405d00ce2f3e15b214c7486c69cbc5bf165cf9596e86f797f62e81914 - Repeat exploit tx:
0x31565843d565ecab7ab65965d180e45a99d4718fa192c2f2221410f65ea03743 - Pre-exploit mint to the sending EOA:
0x935728aefa3419e5e319ad7b68f22940206526f4d4548408642dbbdbd3015f8c - Verified EFVault source collected for
0xbdb515028a6fa6cd1634b5a9651184494abfd336 - Seed tx trace, receipt, and balance-diff artifacts for the first successful redeem
- Auditor derived-state observations showing the mis-scaled storage value, non-incident holder replay, and cumulative receiver payouts