This guide covers the implementation of YearnV3 vault functionality in the CryptoStock frontend, focusing on proper withdrawal flow that matches the test case requirements.
The test case defines the exact flow for YearnV3 operations:
- Authorization: User approves USDT tokens for YearnV3Adapter
- Operation: Execute
defiAggregator.executeOperation("yearnv3", 0, params) - Parameters:
tokens,amounts(USDT amounts),recipient,deadline
- Input Parameter: Withdrawal amount is specified in shares, not USDT
- Authorization: User approves shares to YearnV3Adapter:
await vault.connect(user).approve(yearnAdapter, sharesBefore);
- Operation: Execute withdrawal with shares amount:
const sharesToWithdraw = sharesBefore / 2n; // 50% of shares const withdrawParams = { tokens: [USDT_ADDRESS], amounts: [reasonableWithdrawAmount], // Expected USDT output recipient: user.address, deadline: Math.floor(Date.now() / 1000) + 3600, tokenId: 0, extraData: "0x" }; await defiAggregator.executeOperation("yearnv3", 1, withdrawParams);
Problem: Frontend asks users to input USDT amount for withdrawal Expected: Users should input shares amount (like the test case)
Current Code (YearnV3WithdrawModal.tsx:408):
<label className="block text-sm font-medium text-gray-300 mb-2">
提取数量 (USDT) <!-- ❌ Should be SHARES -->
</label>Problem: previewWithdraw expects shares but receives USDT amounts
Hook Implementation (useYearnV3WithClients.ts:185):
const previewWithdraw = useCallback(async (shares: string) => {
// Expects SHARES amount
const sharesBigInt = parseUnits(shares, TOKEN_DECIMALS.SHARES);
// ...
}, [publicClient, store]);Modal Usage (YearnV3WithdrawModal.tsx:117):
// ❌ Passing USDT amount instead of shares
const preview = await previewWithdraw(value);Problem: withdraw function expects USDT amount but should expect shares
Hook Implementation (useYearnV3WithClients.ts:226):
const withdraw = useCallback(async (amount: string) => {
const amountBigInt = parseUnits(amount, TOKEN_DECIMALS.USDT); // ❌ Should be SHARES
// ...
}, [isConnected, publicClient, chain, getWalletClient, store, address]);File: components/YearnV3WithdrawModal.tsx
Change input label and validation:
// ❌ Current
<label className="block text-sm font-medium text-gray-300 mb-2">
提取数量 (USDT)
</label>
// ✅ Should be
<label className="block text-sm font-medium text-gray-300 mb-2">
提取数量 (Shares)
</label>Update placeholder and validation:
<input
type="text"
value={amount}
onChange={handleAmountChange}
placeholder="0.00" // Should show shares
// ...
/>File: components/YearnV3WithdrawModal.tsx
// ✅ Correct: Use shares directly for preview
const handleAmountChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
if (/^\d*\.?\d{0,6}$/.test(value) || value === '') {
setAmount(value); // This is now SHARES amount
setPercentage(0);
if (validateAmount(value)) {
const timeoutId = setTimeout(async () => {
try {
// ✅ Preview with shares amount
const preview = await previewWithdraw(value);
if (preview.success && preview.data) {
setPreviewData({
assets: preview.data.assets.toString(),
formattedAssets: preview.data.formattedAssets,
requiredShares: value,
inputSharesAmount: value
});
}
} catch (error) {
console.error('预览失败:', error);
setPreviewData(null);
}
}, 300);
return () => clearTimeout(timeoutId);
} else {
setPreviewData(null);
}
}
};File: lib/hooks/useYearnV3WithClients.ts
// ✅ Updated withdrawal function
const withdraw = useCallback(async (sharesAmount: string) => {
if (!isConnected || !address) {
throw new Error('请先连接钱包');
}
// ... validation code ...
try {
store.setOperating(true);
store.setError(null);
console.log('🚀 开始取款操作...', { sharesAmount });
// ✅ Parse as SHARES, not USDT
const sharesBigInt = parseUnits(sharesAmount, TOKEN_DECIMALS.SHARES);
// Preview to get expected USDT amount
const previewResult = await previewWithdraw(sharesAmount);
if (!previewResult.success) {
throw new Error('无法预览取款金额');
}
const expectedUsdtAmount = previewResult.data.assets;
// ✅ Use expected USDT amount for operation params
const operationParams = {
tokens: [DEPLOYMENT_ADDRESSES.usdtToken],
amounts: [expectedUsdtAmount.toString()], // Expected USDT output
recipient: address,
deadline: Math.floor(Date.now() / 1000) + 3600,
tokenId: "0",
extraData: "0x" as const,
};
// Execute withdrawal through DefiAggregator
const hash = await wc.writeContract({
address: defiAggregatorAddress,
abi: typedDefiAggregatorABI,
functionName: 'executeOperation',
args: [
"yearnv3", // Adapter name
YearnV3OperationType.WITHDRAW, // Withdraw operation
operationParams // Operation parameters
],
chain,
account: address,
});
// ... rest of the function ...
} catch (error) {
// ... error handling ...
}
}, [isConnected, publicClient, chain, getWalletClient, store, address, previewWithdraw]);File: components/YearnV3WithdrawModal.tsx
// ✅ Updated validation function
const validateAmount = (value: string): boolean => {
if (!value || parseFloat(value) <= 0) return false;
const maxShares = parseFloat(maxBalances.maxSharesToWithdraw);
return parseFloat(value) <= maxShares;
};
// ✅ Updated max amount handler
const handleMaxAmount = () => {
setAmount(maxBalances.maxSharesToWithdraw); // Max shares, not USDT
setPercentage(100);
};
// ✅ Updated percentage handler
const handlePercentageSelect = (percent: number) => {
setPercentage(percent);
const maxShares = parseFloat(maxBalances.maxSharesToWithdraw);
const selectedShares = (maxShares * percent / 100).toFixed(6);
setAmount(selectedShares);
};File: components/YearnV3WithdrawModal.tsx
// ✅ Updated preview data state type
const [previewData, setPreviewData] = useState<{
assets: string;
formattedAssets: string;
requiredShares: string;
inputSharesAmount: string;
} | null>(null);- User Input: User enters shares amount (e.g., "0.05" shares)
- Validation: Validate against user's available shares balance
- Preview: Call
previewWithdraw(sharesAmount)to get expected USDT - Authorization: Approve shares amount to YearnV3Adapter
- Execution: Call withdrawal with shares amount
- Display: Show expected USDT output to user
// Input section should show:
- Input field: Shares amount (e.g., 0.05)
- Balance display: Available: 0.099799 shares
- Preview: Expected to receive: ~45 USDT
- Percentage buttons: 25%, 50%, 75%, 100% of shares- Deposit: Deposit 100 USDT to get shares
- Check Balance: Verify shares amount displayed correctly
- Withdrawal Test:
- Enter shares amount (e.g., 0.05)
- Verify preview shows expected USDT amount
- Approve shares authorization
- Execute withdrawal
- Verify final balances
- User inputs: "0.05" shares
- System shows: "Expected to receive: ~45 USDT"
- Authorization: Approve 0.05 shares to YearnV3Adapter
- Result: User receives ~45 USDT, shares balance reduced by 0.05
- HIGH: Fix input parameter type (shares vs USDT)
- HIGH: Update preview function usage
- HIGH: Update withdrawal function parameters
- MEDIUM: Update UI labels and displays
- LOW: Add additional validation and error handling
components/YearnV3WithdrawModal.tsx- Main withdrawal modallib/hooks/useYearnV3WithClients.ts- YearnV3 operations hooklib/stores/useYearnV3Store.ts- State managementtest/10-yearnv3-sepolia.test.js- Reference test case
- Mixing USDT and shares amounts - Always be clear which unit you're working with
- Precision errors - Shares use 18 decimals, USDT uses 6 decimals
- Authorization targets - USDT approval goes to adapter, shares approval goes to vault
- Preview vs execution - Preview expects shares, execution expects USDT amounts in params
// Add debug logging to track amounts
console.log('Withdrawal Debug:', {
inputAmount: amount,
inputUnit: 'SHARES',
previewResult: previewData,
expectedUSDT: previewData?.formattedAssets,
authorizationAmount: amount, // Shares amount
});This guide should help implement the withdrawal functionality correctly according to the test case requirements.