Skip to main content

cast/cmd/
erc4626.rs

1use std::str::FromStr;
2
3use crate::{
4    cmd::{erc20::print_amount, rpc_provider, send::SendTxArgs},
5    tx::{SendTxOpts, TxParams},
6};
7use alloy_eips::BlockId;
8use alloy_ens::NameOrAddress;
9use alloy_network::AnyNetwork;
10use alloy_primitives::{Address, FixedBytes, U256, address};
11use alloy_provider::Provider;
12use alloy_sol_types::{SolCall, sol};
13use clap::Parser;
14use eyre::{Result, WrapErr};
15use foundry_cli::{
16    json::{
17        JsonError, JsonMessage, print_json_success, print_json_success_with_warnings, print_scalar,
18    },
19    opts::RpcOpts,
20};
21use foundry_common::{provider::RetryProvider, shell};
22use serde::Serialize;
23
24const NATIVE_ASSET: Address = address!("EeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE");
25/// ERC-7535 asset quantities are denominated in wei.
26const NATIVE_ASSET_DECIMALS: u8 = 18;
27const ERC7540_ASYNC_DEPOSIT_INTERFACE: FixedBytes<4> = FixedBytes::new([0xce, 0x3b, 0xbe, 0x50]);
28const ERC7540_ASYNC_REDEEM_INTERFACE: FixedBytes<4> = FixedBytes::new([0x62, 0x0e, 0xe8, 0xe4]);
29
30sol! {
31    #[sol(rpc)]
32    interface IERC4626 {
33        function asset() external view returns (address assetTokenAddress);
34        function totalAssets() external view returns (uint256 totalManagedAssets);
35        function convertToShares(uint256 assets) external view returns (uint256 shares);
36        function convertToAssets(uint256 shares) external view returns (uint256 assets);
37        function maxDeposit(address receiver) external view returns (uint256 maxAssets);
38        function previewDeposit(uint256 assets) external view returns (uint256 shares);
39        function deposit(uint256 assets, address receiver) external returns (uint256 shares);
40        function maxMint(address receiver) external view returns (uint256 maxShares);
41        function previewMint(uint256 shares) external view returns (uint256 assets);
42        function mint(uint256 shares, address receiver) external returns (uint256 assets);
43        function maxWithdraw(address owner) external view returns (uint256 maxAssets);
44        function previewWithdraw(uint256 assets) external view returns (uint256 shares);
45        function withdraw(uint256 assets, address receiver, address owner)
46            external
47            returns (uint256 shares);
48        function maxRedeem(address owner) external view returns (uint256 maxShares);
49        function previewRedeem(uint256 shares) external view returns (uint256 assets);
50        function redeem(uint256 shares, address receiver, address owner)
51            external
52            returns (uint256 assets);
53
54        function name() external view returns (string);
55        function symbol() external view returns (string);
56        function decimals() external view returns (uint8);
57        function totalSupply() external view returns (uint256 shares);
58        function balanceOf(address owner) external view returns (uint256 shares);
59        function allowance(address owner, address spender) external view returns (uint256 shares);
60    }
61
62    #[sol(rpc)]
63    interface IERC20Metadata {
64        function name() external view returns (string);
65        function symbol() external view returns (string);
66        function decimals() external view returns (uint8);
67        function balanceOf(address owner) external view returns (uint256 amount);
68    }
69
70    #[sol(rpc)]
71    interface IERC165 {
72        function supportsInterface(bytes4 interfaceId) external view returns (bool);
73    }
74}
75
76/// Interact with synchronous ERC-4626 tokenized vaults.
77#[derive(Debug, Parser, Clone)]
78pub enum Erc4626Subcommand {
79    /// Show vault, asset, and exchange-rate information
80    ///
81    /// Example:
82    ///
83    /// ```text
84    /// $ cast erc4626 info 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB --human \
85    ///     --block 25519075 --rpc-url https://ethereum.reth.rs/rpc
86    /// ```
87    ///
88    /// Output:
89    ///
90    /// ```text
91    /// Vault                0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB
92    /// Name                 Steakhouse USDC
93    /// Symbol               steakUSDC
94    /// Decimals             18
95    /// Asset                0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
96    /// Asset name           USD Coin
97    /// Asset symbol         USDC
98    /// Asset decimals       6
99    /// Total assets         95183395.377893 USDC
100    /// Total supply         84037200.060143388288943211 steakUSDC
101    /// Assets per share     1.132634 USDC
102    /// Shares per asset     0.882897731163608580 steakUSDC
103    /// ```
104    #[command(verbatim_doc_comment)]
105    Info {
106        /// The ERC-4626 vault contract address.
107        #[arg(value_parser = NameOrAddress::from_str)]
108        vault: NameOrAddress,
109
110        /// Use formatted token amounts in text output.
111        #[arg(long)]
112        human: bool,
113
114        /// The block height to query at.
115        #[arg(long, short = 'B')]
116        block: Option<BlockId>,
117
118        #[command(flatten)]
119        rpc: RpcOpts,
120    },
121
122    /// Show an account's shares, asset value, and withdrawal limits
123    ///
124    /// Example:
125    ///
126    /// ```text
127    /// $ cast erc4626 position 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB \
128    ///     0x255c7705E8bb334dfCaE438197f7c4297988085A --human --block 25519075 \
129    ///     --rpc-url https://ethereum.reth.rs/rpc
130    /// ```
131    ///
132    /// Output:
133    ///
134    /// ```text
135    /// Vault                0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB
136    /// Owner                0x255c7705e8BB334DfCae438197f7C4297988085a
137    /// Asset                0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
138    /// Share symbol         steakUSDC
139    /// Share decimals       18
140    /// Asset symbol         USDC
141    /// Asset decimals       6
142    /// Share balance        35733.949295544029939485 steakUSDC
143    /// Assets equivalent    40473.486378 USDC
144    /// Max withdraw         40473.486378 USDC
145    /// Max redeem           35733.949295417417957447 steakUSDC
146    /// ```
147    #[command(verbatim_doc_comment)]
148    Position {
149        /// The ERC-4626 vault contract address.
150        #[arg(value_parser = NameOrAddress::from_str)]
151        vault: NameOrAddress,
152
153        /// The owner of the vault shares.
154        #[arg(value_parser = NameOrAddress::from_str)]
155        owner: NameOrAddress,
156
157        /// Use formatted token amounts in text output.
158        #[arg(long)]
159        human: bool,
160
161        /// The block height to query at.
162        #[arg(long, short = 'B')]
163        block: Option<BlockId>,
164
165        #[command(flatten)]
166        rpc: RpcOpts,
167    },
168
169    /// Probe synchronous ERC-4626 interface compatibility
170    ///
171    /// Example:
172    ///
173    /// ```text
174    /// $ cast erc4626 check 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB \
175    ///     --account 0x255c7705E8bb334dfCaE438197f7c4297988085A --block 25519075 \
176    ///     --rpc-url https://ethereum.reth.rs/rpc
177    /// ```
178    ///
179    /// Output:
180    ///
181    /// ```text
182    /// Vault                0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB
183    /// Account              0x255c7705e8BB334DfCae438197f7C4297988085a
184    /// Note: This probes read-call behavior only; it does not prove state-changing selector coverage or semantic ERC-4626 compliance.
185    /// PASS contract code            contract bytecode is present
186    /// PASS asset()                  returned 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
187    /// PASS asset contract           underlying asset bytecode is present
188    /// PASS asset balanceOf(address) call succeeded
189    /// PASS totalAssets()            call succeeded
190    /// PASS totalSupply()            call succeeded
191    /// PASS balanceOf(address)       call succeeded
192    /// PASS allowance(address,address) call succeeded
193    /// PASS convertToShares(0)       returned zero
194    /// PASS convertToAssets(0)       returned zero
195    /// PASS maxDeposit(address)      call succeeded
196    /// PASS previewDeposit(0)        returned zero
197    /// PASS maxMint(address)         call succeeded
198    /// PASS previewMint(0)           returned zero
199    /// PASS maxWithdraw(address)     call succeeded
200    /// PASS previewWithdraw(0)       returned zero
201    /// PASS maxRedeem(address)       call succeeded
202    /// PASS previewRedeem(0)         returned zero
203    /// PASS name()                   call succeeded
204    /// PASS symbol()                 call succeeded
205    /// PASS decimals()               call succeeded
206    /// Summary: 21 passed, 0 warnings, 0 failed
207    /// ```
208    #[command(verbatim_doc_comment)]
209    Check {
210        /// The ERC-4626 vault contract address.
211        #[arg(value_parser = NameOrAddress::from_str)]
212        vault: NameOrAddress,
213
214        /// The account used for limit and balance probes.
215        #[arg(long, value_parser = NameOrAddress::from_str)]
216        account: Option<NameOrAddress>,
217
218        /// The block height to query at.
219        #[arg(long, short = 'B')]
220        block: Option<BlockId>,
221
222        #[command(flatten)]
223        rpc: RpcOpts,
224    },
225
226    /// Query the vault's underlying asset token
227    ///
228    /// Example:
229    ///
230    /// ```text
231    /// $ cast erc4626 asset 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB \
232    ///     --block 25519075 --rpc-url https://ethereum.reth.rs/rpc
233    /// ```
234    ///
235    /// Output:
236    ///
237    /// ```text
238    /// 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
239    /// ```
240    #[command(verbatim_doc_comment)]
241    Asset {
242        /// The ERC-4626 vault contract address.
243        #[arg(value_parser = NameOrAddress::from_str)]
244        vault: NameOrAddress,
245
246        /// The block height to query at.
247        #[arg(long, short = 'B')]
248        block: Option<BlockId>,
249
250        #[command(flatten)]
251        rpc: RpcOpts,
252    },
253
254    /// Query the total amount of underlying assets managed by the vault
255    ///
256    /// Example:
257    ///
258    /// ```text
259    /// $ cast erc4626 total-assets 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB \
260    ///     --block 25519075 --rpc-url https://ethereum.reth.rs/rpc
261    /// ```
262    ///
263    /// Output:
264    ///
265    /// ```text
266    /// 95183395377893 [9.518e13]
267    /// ```
268    #[command(verbatim_doc_comment)]
269    TotalAssets {
270        /// The ERC-4626 vault contract address.
271        #[arg(value_parser = NameOrAddress::from_str)]
272        vault: NameOrAddress,
273
274        /// The block height to query at.
275        #[arg(long, short = 'B')]
276        block: Option<BlockId>,
277
278        #[command(flatten)]
279        rpc: RpcOpts,
280    },
281
282    /// Convert an asset amount to the corresponding share amount
283    ///
284    /// Example:
285    ///
286    /// ```text
287    /// $ cast erc4626 convert-to-shares 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB \
288    ///     1000000 --block 25519075 --rpc-url https://ethereum.reth.rs/rpc
289    /// ```
290    ///
291    /// Output:
292    ///
293    /// ```text
294    /// 882897731163608580 [8.828e17]
295    /// ```
296    #[command(verbatim_doc_comment)]
297    ConvertToShares {
298        /// The ERC-4626 vault contract address.
299        #[arg(value_parser = NameOrAddress::from_str)]
300        vault: NameOrAddress,
301
302        /// The amount of underlying assets.
303        assets: U256,
304
305        /// The block height to query at.
306        #[arg(long, short = 'B')]
307        block: Option<BlockId>,
308
309        #[command(flatten)]
310        rpc: RpcOpts,
311    },
312
313    /// Convert a share amount to the corresponding asset amount
314    ///
315    /// Example:
316    ///
317    /// ```text
318    /// $ cast erc4626 convert-to-assets 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB \
319    ///     1000000000000000000 --block 25519075 \
320    ///     --rpc-url https://ethereum.reth.rs/rpc
321    /// ```
322    ///
323    /// Output:
324    ///
325    /// ```text
326    /// 1132634 [1.132e6]
327    /// ```
328    #[command(verbatim_doc_comment)]
329    ConvertToAssets {
330        /// The ERC-4626 vault contract address.
331        #[arg(value_parser = NameOrAddress::from_str)]
332        vault: NameOrAddress,
333
334        /// The amount of vault shares.
335        shares: U256,
336
337        /// The block height to query at.
338        #[arg(long, short = 'B')]
339        block: Option<BlockId>,
340
341        #[command(flatten)]
342        rpc: RpcOpts,
343    },
344
345    /// Query the maximum assets that may be deposited for a receiver
346    ///
347    /// Example:
348    ///
349    /// ```text
350    /// $ cast erc4626 max-deposit 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB \
351    ///     0x255c7705E8bb334dfCaE438197f7c4297988085A --block 25519075 \
352    ///     --rpc-url https://ethereum.reth.rs/rpc
353    /// ```
354    ///
355    /// Output:
356    ///
357    /// ```text
358    /// 1002934816604622098 [1.002e18]
359    /// ```
360    #[command(verbatim_doc_comment)]
361    MaxDeposit {
362        /// The ERC-4626 vault contract address.
363        #[arg(value_parser = NameOrAddress::from_str)]
364        vault: NameOrAddress,
365
366        /// The receiver of the resulting vault shares.
367        #[arg(value_parser = NameOrAddress::from_str)]
368        receiver: NameOrAddress,
369
370        /// The block height to query at.
371        #[arg(long, short = 'B')]
372        block: Option<BlockId>,
373
374        #[command(flatten)]
375        rpc: RpcOpts,
376    },
377
378    /// Preview the shares received by depositing an asset amount
379    ///
380    /// Example:
381    ///
382    /// ```text
383    /// $ cast erc4626 preview-deposit 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB \
384    ///     1000000 --block 25519075 --rpc-url https://ethereum.reth.rs/rpc
385    /// ```
386    ///
387    /// Output:
388    ///
389    /// ```text
390    /// 882897731163608580 [8.828e17]
391    /// ```
392    #[command(verbatim_doc_comment)]
393    PreviewDeposit {
394        /// The ERC-4626 vault contract address.
395        #[arg(value_parser = NameOrAddress::from_str)]
396        vault: NameOrAddress,
397
398        /// The amount of underlying assets.
399        assets: U256,
400
401        /// The block height to query at.
402        #[arg(long, short = 'B')]
403        block: Option<BlockId>,
404
405        #[command(flatten)]
406        rpc: RpcOpts,
407    },
408
409    /// Deposit assets into the vault
410    ///
411    /// The vault must have sufficient allowance to spend the underlying asset.
412    ///
413    /// Example:
414    ///
415    /// ```text
416    /// $ cast erc4626 deposit 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB \
417    ///     1000000 $ACCOUNT --private-key $PRIVATE_KEY --async --rpc-url $ETH_RPC_URL
418    /// ```
419    ///
420    /// Output:
421    ///
422    /// ```text
423    /// 0x6f2a7e10f148a0ee81208cd8d7dee10cc33b5bdb739bfeef805dc68467e6db4e
424    /// ```
425    #[command(verbatim_doc_comment)]
426    Deposit {
427        /// The ERC-4626 vault contract address.
428        #[arg(value_parser = NameOrAddress::from_str)]
429        vault: NameOrAddress,
430
431        /// The amount of underlying assets.
432        assets: U256,
433
434        /// The receiver of the resulting vault shares.
435        #[arg(value_parser = NameOrAddress::from_str)]
436        receiver: NameOrAddress,
437
438        #[command(flatten)]
439        send_tx: SendTxOpts,
440
441        #[command(flatten)]
442        tx: TxParams,
443    },
444
445    /// Query the maximum shares that may be minted for a receiver
446    ///
447    /// Example:
448    ///
449    /// ```text
450    /// $ cast erc4626 max-mint 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB \
451    ///     0x255c7705E8bb334dfCaE438197f7c4297988085A --block 25519075 \
452    ///     --rpc-url https://ethereum.reth.rs/rpc
453    /// ```
454    ///
455    /// Output:
456    ///
457    /// ```text
458    /// 885488874085210715750045480687 [8.854e29]
459    /// ```
460    #[command(verbatim_doc_comment)]
461    MaxMint {
462        /// The ERC-4626 vault contract address.
463        #[arg(value_parser = NameOrAddress::from_str)]
464        vault: NameOrAddress,
465
466        /// The receiver of the resulting vault shares.
467        #[arg(value_parser = NameOrAddress::from_str)]
468        receiver: NameOrAddress,
469
470        /// The block height to query at.
471        #[arg(long, short = 'B')]
472        block: Option<BlockId>,
473
474        #[command(flatten)]
475        rpc: RpcOpts,
476    },
477
478    /// Preview the assets required to mint a share amount
479    ///
480    /// Example:
481    ///
482    /// ```text
483    /// $ cast erc4626 preview-mint 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB \
484    ///     1000000000000000000 --block 25519075 \
485    ///     --rpc-url https://ethereum.reth.rs/rpc
486    /// ```
487    ///
488    /// Output:
489    ///
490    /// ```text
491    /// 1132635 [1.132e6]
492    /// ```
493    #[command(verbatim_doc_comment)]
494    PreviewMint {
495        /// The ERC-4626 vault contract address.
496        #[arg(value_parser = NameOrAddress::from_str)]
497        vault: NameOrAddress,
498
499        /// The amount of vault shares.
500        shares: U256,
501
502        /// The block height to query at.
503        #[arg(long, short = 'B')]
504        block: Option<BlockId>,
505
506        #[command(flatten)]
507        rpc: RpcOpts,
508    },
509
510    /// Mint vault shares
511    ///
512    /// The vault must have sufficient allowance to spend the underlying asset.
513    ///
514    /// Example:
515    ///
516    /// ```text
517    /// $ cast erc4626 mint 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB \
518    ///     1000000000000000000 $ACCOUNT --private-key $PRIVATE_KEY --async \
519    ///     --rpc-url $ETH_RPC_URL
520    /// ```
521    ///
522    /// Output:
523    ///
524    /// ```text
525    /// 0xa7c3b4e26a99e2cb422e2b36cffeae642a39f1d09f8f3454c9c2d4657ebb0491
526    /// ```
527    #[command(verbatim_doc_comment)]
528    Mint {
529        /// The ERC-4626 vault contract address.
530        #[arg(value_parser = NameOrAddress::from_str)]
531        vault: NameOrAddress,
532
533        /// The amount of vault shares.
534        shares: U256,
535
536        /// The receiver of the resulting vault shares.
537        #[arg(value_parser = NameOrAddress::from_str)]
538        receiver: NameOrAddress,
539
540        #[command(flatten)]
541        send_tx: SendTxOpts,
542
543        #[command(flatten)]
544        tx: TxParams,
545    },
546
547    /// Query the maximum assets that an owner may withdraw
548    ///
549    /// Example:
550    ///
551    /// ```text
552    /// $ cast erc4626 max-withdraw 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB \
553    ///     0x255c7705E8bb334dfCaE438197f7c4297988085A --block 25519075 \
554    ///     --rpc-url https://ethereum.reth.rs/rpc
555    /// ```
556    ///
557    /// Output:
558    ///
559    /// ```text
560    /// 40473486378 [4.047e10]
561    /// ```
562    #[command(verbatim_doc_comment)]
563    MaxWithdraw {
564        /// The ERC-4626 vault contract address.
565        #[arg(value_parser = NameOrAddress::from_str)]
566        vault: NameOrAddress,
567
568        /// The owner of the vault shares.
569        #[arg(value_parser = NameOrAddress::from_str)]
570        owner: NameOrAddress,
571
572        /// The block height to query at.
573        #[arg(long, short = 'B')]
574        block: Option<BlockId>,
575
576        #[command(flatten)]
577        rpc: RpcOpts,
578    },
579
580    /// Preview the shares burned to withdraw an asset amount
581    ///
582    /// Example:
583    ///
584    /// ```text
585    /// $ cast erc4626 preview-withdraw 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB \
586    ///     1000000 --block 25519075 --rpc-url https://ethereum.reth.rs/rpc
587    /// ```
588    ///
589    /// Output:
590    ///
591    /// ```text
592    /// 882897731163608581 [8.828e17]
593    /// ```
594    #[command(verbatim_doc_comment)]
595    PreviewWithdraw {
596        /// The ERC-4626 vault contract address.
597        #[arg(value_parser = NameOrAddress::from_str)]
598        vault: NameOrAddress,
599
600        /// The amount of underlying assets.
601        assets: U256,
602
603        /// The block height to query at.
604        #[arg(long, short = 'B')]
605        block: Option<BlockId>,
606
607        #[command(flatten)]
608        rpc: RpcOpts,
609    },
610
611    /// Withdraw assets from the vault
612    ///
613    /// Example:
614    ///
615    /// ```text
616    /// $ cast erc4626 withdraw 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB \
617    ///     1000000 $ACCOUNT $ACCOUNT --private-key $PRIVATE_KEY --async \
618    ///     --rpc-url $ETH_RPC_URL
619    /// ```
620    ///
621    /// Output:
622    ///
623    /// ```text
624    /// 0x6d6d70151151f30aa13bdb4082ea7fd3e531193eebdfbb356586284cd0e6a8a2
625    /// ```
626    #[command(verbatim_doc_comment)]
627    Withdraw {
628        /// The ERC-4626 vault contract address.
629        #[arg(value_parser = NameOrAddress::from_str)]
630        vault: NameOrAddress,
631
632        /// The amount of underlying assets.
633        assets: U256,
634
635        /// The receiver of the withdrawn assets.
636        #[arg(value_parser = NameOrAddress::from_str)]
637        receiver: NameOrAddress,
638
639        /// The owner of the vault shares.
640        #[arg(value_parser = NameOrAddress::from_str)]
641        owner: NameOrAddress,
642
643        #[command(flatten)]
644        send_tx: SendTxOpts,
645
646        #[command(flatten)]
647        tx: TxParams,
648    },
649
650    /// Query the maximum shares that an owner may redeem
651    ///
652    /// Example:
653    ///
654    /// ```text
655    /// $ cast erc4626 max-redeem 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB \
656    ///     0x255c7705E8bb334dfCaE438197f7c4297988085A --block 25519075 \
657    ///     --rpc-url https://ethereum.reth.rs/rpc
658    /// ```
659    ///
660    /// Output:
661    ///
662    /// ```text
663    /// 35733949295417417957447 [3.573e22]
664    /// ```
665    #[command(verbatim_doc_comment)]
666    MaxRedeem {
667        /// The ERC-4626 vault contract address.
668        #[arg(value_parser = NameOrAddress::from_str)]
669        vault: NameOrAddress,
670
671        /// The owner of the vault shares.
672        #[arg(value_parser = NameOrAddress::from_str)]
673        owner: NameOrAddress,
674
675        /// The block height to query at.
676        #[arg(long, short = 'B')]
677        block: Option<BlockId>,
678
679        #[command(flatten)]
680        rpc: RpcOpts,
681    },
682
683    /// Preview the assets received by redeeming a share amount
684    ///
685    /// Example:
686    ///
687    /// ```text
688    /// $ cast erc4626 preview-redeem 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB \
689    ///     1000000000000000000 --block 25519075 \
690    ///     --rpc-url https://ethereum.reth.rs/rpc
691    /// ```
692    ///
693    /// Output:
694    ///
695    /// ```text
696    /// 1132634 [1.132e6]
697    /// ```
698    #[command(verbatim_doc_comment)]
699    PreviewRedeem {
700        /// The ERC-4626 vault contract address.
701        #[arg(value_parser = NameOrAddress::from_str)]
702        vault: NameOrAddress,
703
704        /// The amount of vault shares.
705        shares: U256,
706
707        /// The block height to query at.
708        #[arg(long, short = 'B')]
709        block: Option<BlockId>,
710
711        #[command(flatten)]
712        rpc: RpcOpts,
713    },
714
715    /// Redeem vault shares for assets
716    ///
717    /// Example:
718    ///
719    /// ```text
720    /// $ cast erc4626 redeem 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB \
721    ///     1000000000000000000 $ACCOUNT $ACCOUNT --private-key $PRIVATE_KEY --async \
722    ///     --rpc-url $ETH_RPC_URL
723    /// ```
724    ///
725    /// Output:
726    ///
727    /// ```text
728    /// 0x304d463e4d33b462778a974d008cd1b9c4c109730ae6617d2815477b8aa7c03e
729    /// ```
730    #[command(verbatim_doc_comment)]
731    Redeem {
732        /// The ERC-4626 vault contract address.
733        #[arg(value_parser = NameOrAddress::from_str)]
734        vault: NameOrAddress,
735
736        /// The amount of vault shares.
737        shares: U256,
738
739        /// The receiver of the redeemed assets.
740        #[arg(value_parser = NameOrAddress::from_str)]
741        receiver: NameOrAddress,
742
743        /// The owner of the vault shares.
744        #[arg(value_parser = NameOrAddress::from_str)]
745        owner: NameOrAddress,
746
747        #[command(flatten)]
748        send_tx: SendTxOpts,
749
750        #[command(flatten)]
751        tx: TxParams,
752    },
753}
754
755#[derive(Debug, Serialize)]
756struct TokenAmount {
757    raw: String,
758    #[serde(skip_serializing_if = "Option::is_none")]
759    formatted: Option<String>,
760}
761
762impl TokenAmount {
763    fn new(value: U256, decimals: Option<u8>) -> Self {
764        Self {
765            raw: value.to_string(),
766            formatted: decimals
767                .and_then(|decimals| crate::args::format_units(&value.to_string(), decimals).ok()),
768        }
769    }
770}
771
772#[derive(Debug, Serialize)]
773struct VaultInfo {
774    vault: String,
775    name: Option<String>,
776    symbol: Option<String>,
777    decimals: Option<u8>,
778    asset: String,
779    asset_name: Option<String>,
780    asset_symbol: Option<String>,
781    asset_decimals: Option<u8>,
782    total_assets: TokenAmount,
783    total_supply: TokenAmount,
784    assets_per_share: Option<TokenAmount>,
785    shares_per_asset: Option<TokenAmount>,
786}
787
788#[derive(Debug, Serialize)]
789struct VaultPosition {
790    vault: String,
791    owner: String,
792    asset: String,
793    share_symbol: Option<String>,
794    share_decimals: Option<u8>,
795    asset_symbol: Option<String>,
796    asset_decimals: Option<u8>,
797    share_balance: TokenAmount,
798    assets_equivalent: TokenAmount,
799    max_withdraw: TokenAmount,
800    max_redeem: TokenAmount,
801}
802
803#[derive(Debug)]
804struct VaultWarning {
805    code: &'static str,
806    message: String,
807}
808
809#[derive(Debug, Clone, Copy, Serialize)]
810#[serde(rename_all = "lowercase")]
811enum CheckStatus {
812    Pass,
813    Warn,
814    Fail,
815}
816
817#[derive(Debug, Serialize)]
818struct CompatibilityCheck {
819    name: String,
820    status: CheckStatus,
821    detail: String,
822}
823
824#[derive(Debug, Serialize)]
825struct CompatibilityReport {
826    vault: String,
827    account: String,
828    read_compatible: bool,
829    disclaimer: &'static str,
830    passed: usize,
831    warnings: usize,
832    failed: usize,
833    checks: Vec<CompatibilityCheck>,
834}
835
836const CHECK_DISCLAIMER: &str = "This probes read-call behavior only; it does not prove state-changing selector coverage or \
837     semantic ERC-4626 compliance.";
838
839impl Erc4626Subcommand {
840    pub async fn run(self) -> Result<()> {
841        match self {
842            Self::Info { vault, human, block, rpc } => show_info(vault, human, block, rpc).await,
843            Self::Position { vault, owner, human, block, rpc } => {
844                show_position(vault, owner, human, block, rpc).await
845            }
846            Self::Check { vault, account, block, rpc } => {
847                check_compatibility(vault, account, block, rpc).await
848            }
849            Self::Asset { vault, block, rpc } => {
850                let (_, vault) = vault_at(&rpc, vault).await?;
851                let asset = vault.asset().block(block.unwrap_or_default()).call().await?;
852                warn_if_native_asset(asset)?;
853                print_scalar(asset.to_string())
854            }
855            Self::TotalAssets { vault, block, rpc } => {
856                let (_, vault) = vault_at(&rpc, vault).await?;
857                print_amount(vault.totalAssets().block(block.unwrap_or_default()).call().await?)
858            }
859            Self::ConvertToShares { vault, assets, block, rpc } => {
860                let (_, vault) = vault_at(&rpc, vault).await?;
861                let call = vault.convertToShares(assets).block(block.unwrap_or_default());
862                print_amount(call.call().await?)
863            }
864            Self::ConvertToAssets { vault, shares, block, rpc } => {
865                let (_, vault) = vault_at(&rpc, vault).await?;
866                let call = vault.convertToAssets(shares).block(block.unwrap_or_default());
867                print_amount(call.call().await?)
868            }
869            Self::MaxDeposit { vault, receiver, block, rpc } => {
870                let (provider, vault) = vault_at(&rpc, vault).await?;
871                let receiver = receiver.resolve(&provider).await?;
872                let assets =
873                    vault.maxDeposit(receiver).block(block.unwrap_or_default()).call().await?;
874                warn_if_zero_entry_max("maxDeposit", assets)?;
875                print_amount(assets)
876            }
877            Self::PreviewDeposit { vault, assets, block, rpc } => {
878                let (_, vault) = vault_at(&rpc, vault).await?;
879                let call = vault.previewDeposit(assets).block(block.unwrap_or_default());
880                print_amount(call.call().await.wrap_err_with(|| preview_error("Deposit"))?)
881            }
882            Self::Deposit { vault, assets, receiver, send_tx, tx } => {
883                let [receiver] = prepare_write(&vault, [receiver], &send_tx).await?;
884                send_call(vault, IERC4626::depositCall { assets, receiver }, send_tx, tx).await
885            }
886            Self::MaxMint { vault, receiver, block, rpc } => {
887                let (provider, vault) = vault_at(&rpc, vault).await?;
888                let receiver = receiver.resolve(&provider).await?;
889                let shares =
890                    vault.maxMint(receiver).block(block.unwrap_or_default()).call().await?;
891                warn_if_zero_entry_max("maxMint", shares)?;
892                print_amount(shares)
893            }
894            Self::PreviewMint { vault, shares, block, rpc } => {
895                let (_, vault) = vault_at(&rpc, vault).await?;
896                let call = vault.previewMint(shares).block(block.unwrap_or_default());
897                print_amount(call.call().await.wrap_err_with(|| preview_error("Mint"))?)
898            }
899            Self::Mint { vault, shares, receiver, send_tx, tx } => {
900                let [receiver] = prepare_write(&vault, [receiver], &send_tx).await?;
901                send_call(vault, IERC4626::mintCall { shares, receiver }, send_tx, tx).await
902            }
903            Self::MaxWithdraw { vault, owner, block, rpc } => {
904                let (provider, vault) = vault_at(&rpc, vault).await?;
905                let owner = owner.resolve(&provider).await?;
906                let block = block.unwrap_or_default();
907                let assets = vault.maxWithdraw(owner).block(block).call().await?;
908                if assets.is_zero() && has_shares(&vault, owner, block).await {
909                    warn_if_zero_exit_max("maxWithdraw")?;
910                }
911                print_amount(assets)
912            }
913            Self::PreviewWithdraw { vault, assets, block, rpc } => {
914                let (_, vault) = vault_at(&rpc, vault).await?;
915                let call = vault.previewWithdraw(assets).block(block.unwrap_or_default());
916                print_amount(call.call().await.wrap_err_with(|| preview_error("Withdraw"))?)
917            }
918            Self::Withdraw { vault, assets, receiver, owner, send_tx, tx } => {
919                let [receiver, owner] = prepare_write(&vault, [receiver, owner], &send_tx).await?;
920                let call = IERC4626::withdrawCall { assets, receiver, owner };
921                send_call(vault, call, send_tx, tx).await
922            }
923            Self::MaxRedeem { vault, owner, block, rpc } => {
924                let (provider, vault) = vault_at(&rpc, vault).await?;
925                let owner = owner.resolve(&provider).await?;
926                let block = block.unwrap_or_default();
927                let shares = vault.maxRedeem(owner).block(block).call().await?;
928                if shares.is_zero() && has_shares(&vault, owner, block).await {
929                    warn_if_zero_exit_max("maxRedeem")?;
930                }
931                print_amount(shares)
932            }
933            Self::PreviewRedeem { vault, shares, block, rpc } => {
934                let (_, vault) = vault_at(&rpc, vault).await?;
935                let call = vault.previewRedeem(shares).block(block.unwrap_or_default());
936                print_amount(call.call().await.wrap_err_with(|| preview_error("Redeem"))?)
937            }
938            Self::Redeem { vault, shares, receiver, owner, send_tx, tx } => {
939                let [receiver, owner] = prepare_write(&vault, [receiver, owner], &send_tx).await?;
940                let call = IERC4626::redeemCall { shares, receiver, owner };
941                send_call(vault, call, send_tx, tx).await
942            }
943        }
944    }
945}
946
947type Vault = IERC4626::IERC4626Instance<RetryProvider, AnyNetwork>;
948
949async fn vault_at(rpc: &RpcOpts, vault: NameOrAddress) -> Result<(RetryProvider, Vault)> {
950    let provider = rpc_provider(rpc)?;
951    let vault = vault.resolve(&provider).await?;
952    Ok((provider.clone(), IERC4626::new(vault, provider)))
953}
954
955async fn has_shares(vault: &Vault, owner: Address, block: BlockId) -> bool {
956    vault.balanceOf(owner).block(block).call().await.is_ok_and(|shares| !shares.is_zero())
957}
958
959/// Error context for a failed `preview*` call; asynchronous ERC-7540 vaults revert these.
960fn preview_error(method: &str) -> String {
961    let kind = if matches!(method, "Deposit" | "Mint") { "deposit" } else { "redeem" };
962    format!(
963        "preview{method} failed; asynchronous ERC-7540 {kind} vaults intentionally revert this \
964         preview"
965    )
966}
967
968async fn show_info(
969    vault: NameOrAddress,
970    human: bool,
971    block: Option<BlockId>,
972    rpc: RpcOpts,
973) -> Result<()> {
974    let (provider, contract) = vault_at(&rpc, vault).await?;
975    let vault = *contract.address();
976    let block = block.unwrap_or_default();
977
978    let name_call = contract.name().block(block);
979    let symbol_call = contract.symbol().block(block);
980    let decimals_call = contract.decimals().block(block);
981    let asset_call = contract.asset().block(block);
982    let total_assets_call = contract.totalAssets().block(block);
983    let total_supply_call = contract.totalSupply().block(block);
984    let (name, symbol, decimals, asset, total_assets, total_supply) = tokio::join!(
985        name_call.call(),
986        symbol_call.call(),
987        decimals_call.call(),
988        asset_call.call(),
989        total_assets_call.call(),
990        total_supply_call.call(),
991    );
992
993    let asset = asset.wrap_err("asset() call failed")?;
994    let total_assets = total_assets.wrap_err("totalAssets() call failed")?;
995    let total_supply = total_supply.wrap_err("totalSupply() call failed")?;
996    let name = name.ok();
997    let symbol = symbol.ok();
998    let decimals = decimals.ok();
999
1000    let mut warnings = Vec::new();
1001    let (asset_name, asset_symbol, asset_decimals) = if asset == NATIVE_ASSET {
1002        warnings.push(native_asset_warning());
1003        (None, None, Some(NATIVE_ASSET_DECIMALS))
1004    } else {
1005        let asset_contract = IERC20Metadata::new(asset, &provider);
1006        let name_call = asset_contract.name().block(block);
1007        let symbol_call = asset_contract.symbol().block(block);
1008        let decimals_call = asset_contract.decimals().block(block);
1009        let (name, symbol, decimals) =
1010            tokio::join!(name_call.call(), symbol_call.call(), decimals_call.call());
1011        (name.ok(), symbol.ok(), decimals.ok())
1012    };
1013
1014    let assets_per_share = match decimals.and_then(decimal_unit) {
1015        Some(unit) => contract.convertToAssets(unit).block(block).call().await.ok(),
1016        None => None,
1017    }
1018    .map(|value| TokenAmount::new(value, asset_decimals));
1019    let shares_per_asset = match asset_decimals.and_then(decimal_unit) {
1020        Some(unit) => contract.convertToShares(unit).block(block).call().await.ok(),
1021        None => None,
1022    }
1023    .map(|value| TokenAmount::new(value, decimals));
1024
1025    print_info(
1026        VaultInfo {
1027            vault: vault.to_string(),
1028            name,
1029            symbol,
1030            decimals,
1031            asset: asset.to_string(),
1032            asset_name,
1033            asset_symbol,
1034            asset_decimals,
1035            total_assets: TokenAmount::new(total_assets, asset_decimals),
1036            total_supply: TokenAmount::new(total_supply, decimals),
1037            assets_per_share,
1038            shares_per_asset,
1039        },
1040        human,
1041        warnings,
1042    )
1043}
1044
1045async fn show_position(
1046    vault: NameOrAddress,
1047    owner: NameOrAddress,
1048    human: bool,
1049    block: Option<BlockId>,
1050    rpc: RpcOpts,
1051) -> Result<()> {
1052    let (provider, contract) = vault_at(&rpc, vault).await?;
1053    let vault = *contract.address();
1054    let owner = owner.resolve(&provider).await?;
1055    let block = block.unwrap_or_default();
1056
1057    let asset_call = contract.asset().block(block);
1058    let symbol_call = contract.symbol().block(block);
1059    let decimals_call = contract.decimals().block(block);
1060    let balance_call = contract.balanceOf(owner).block(block);
1061    let max_withdraw_call = contract.maxWithdraw(owner).block(block);
1062    let max_redeem_call = contract.maxRedeem(owner).block(block);
1063    let (asset, share_symbol, share_decimals, share_balance, max_withdraw, max_redeem) = tokio::join!(
1064        asset_call.call(),
1065        symbol_call.call(),
1066        decimals_call.call(),
1067        balance_call.call(),
1068        max_withdraw_call.call(),
1069        max_redeem_call.call(),
1070    );
1071
1072    let asset = asset.wrap_err("asset() call failed")?;
1073    let share_balance = share_balance.wrap_err("balanceOf() call failed")?;
1074    let max_withdraw = max_withdraw.wrap_err("maxWithdraw() call failed")?;
1075    let max_redeem = max_redeem.wrap_err("maxRedeem() call failed")?;
1076    let share_symbol = share_symbol.ok();
1077    let share_decimals = share_decimals.ok();
1078    let assets_equivalent = contract
1079        .convertToAssets(share_balance)
1080        .block(block)
1081        .call()
1082        .await
1083        .wrap_err("convertToAssets() call failed")?;
1084
1085    let mut warnings = Vec::new();
1086    let (asset_symbol, asset_decimals) = if asset == NATIVE_ASSET {
1087        warnings.push(native_asset_warning());
1088        (None, Some(NATIVE_ASSET_DECIMALS))
1089    } else {
1090        let asset_contract = IERC20Metadata::new(asset, &provider);
1091        let symbol_call = asset_contract.symbol().block(block);
1092        let decimals_call = asset_contract.decimals().block(block);
1093        let (symbol, decimals) = tokio::join!(symbol_call.call(), decimals_call.call());
1094        (symbol.ok(), decimals.ok())
1095    };
1096
1097    if !share_balance.is_zero() {
1098        if max_withdraw.is_zero() {
1099            warnings.push(zero_exit_warning("maxWithdraw"));
1100        }
1101        if max_redeem.is_zero() {
1102            warnings.push(zero_exit_warning("maxRedeem"));
1103        }
1104    }
1105
1106    print_position(
1107        VaultPosition {
1108            vault: vault.to_string(),
1109            owner: owner.to_string(),
1110            asset: asset.to_string(),
1111            share_symbol,
1112            share_decimals,
1113            asset_symbol,
1114            asset_decimals,
1115            share_balance: TokenAmount::new(share_balance, share_decimals),
1116            assets_equivalent: TokenAmount::new(assets_equivalent, asset_decimals),
1117            max_withdraw: TokenAmount::new(max_withdraw, asset_decimals),
1118            max_redeem: TokenAmount::new(max_redeem, share_decimals),
1119        },
1120        human,
1121        warnings,
1122    )
1123}
1124
1125async fn check_compatibility(
1126    vault: NameOrAddress,
1127    account: Option<NameOrAddress>,
1128    block: Option<BlockId>,
1129    rpc: RpcOpts,
1130) -> Result<()> {
1131    let (provider, contract) = vault_at(&rpc, vault).await?;
1132    let vault = *contract.address();
1133    let account = match account {
1134        Some(account) => account.resolve(&provider).await?,
1135        None => Address::ZERO,
1136    };
1137    let block = block.unwrap_or_default();
1138
1139    let code_call = provider.get_code_at(vault).block_id(block);
1140    let asset_call = contract.asset().block(block);
1141    let total_assets_call = contract.totalAssets().block(block);
1142    let convert_to_shares_call = contract.convertToShares(U256::ZERO).block(block);
1143    let convert_to_assets_call = contract.convertToAssets(U256::ZERO).block(block);
1144    let max_deposit_call = contract.maxDeposit(account).block(block);
1145    let preview_deposit_call = contract.previewDeposit(U256::ZERO).block(block);
1146    let max_mint_call = contract.maxMint(account).block(block);
1147    let preview_mint_call = contract.previewMint(U256::ZERO).block(block);
1148    let max_withdraw_call = contract.maxWithdraw(account).block(block);
1149    let preview_withdraw_call = contract.previewWithdraw(U256::ZERO).block(block);
1150    let max_redeem_call = contract.maxRedeem(account).block(block);
1151    let preview_redeem_call = contract.previewRedeem(U256::ZERO).block(block);
1152    let name_call = contract.name().block(block);
1153    let symbol_call = contract.symbol().block(block);
1154    let decimals_call = contract.decimals().block(block);
1155    let total_supply_call = contract.totalSupply().block(block);
1156    let balance_call = contract.balanceOf(account).block(block);
1157    let allowance_call = contract.allowance(account, vault).block(block);
1158    let erc165 = IERC165::new(vault, &provider);
1159    let async_deposit_call = erc165.supportsInterface(ERC7540_ASYNC_DEPOSIT_INTERFACE).block(block);
1160    let async_redeem_call = erc165.supportsInterface(ERC7540_ASYNC_REDEEM_INTERFACE).block(block);
1161    let (
1162        code,
1163        asset,
1164        total_assets,
1165        convert_to_shares,
1166        convert_to_assets,
1167        max_deposit,
1168        preview_deposit,
1169        max_mint,
1170        preview_mint,
1171        max_withdraw,
1172        preview_withdraw,
1173        max_redeem,
1174        preview_redeem,
1175        name,
1176        symbol,
1177        decimals,
1178        total_supply,
1179        balance,
1180        allowance,
1181        async_deposit,
1182        async_redeem,
1183    ) = tokio::join!(
1184        code_call,
1185        asset_call.call(),
1186        total_assets_call.call(),
1187        convert_to_shares_call.call(),
1188        convert_to_assets_call.call(),
1189        max_deposit_call.call(),
1190        preview_deposit_call.call(),
1191        max_mint_call.call(),
1192        preview_mint_call.call(),
1193        max_withdraw_call.call(),
1194        preview_withdraw_call.call(),
1195        max_redeem_call.call(),
1196        preview_redeem_call.call(),
1197        name_call.call(),
1198        symbol_call.call(),
1199        decimals_call.call(),
1200        total_supply_call.call(),
1201        balance_call.call(),
1202        allowance_call.call(),
1203        async_deposit_call.call(),
1204        async_redeem_call.call(),
1205    );
1206    let async_deposit = async_deposit.unwrap_or(false);
1207    let async_redeem = async_redeem.unwrap_or(false);
1208
1209    let mut checks = Vec::new();
1210    match code {
1211        Ok(code) if !code.is_empty() => push_check(
1212            &mut checks,
1213            "contract code",
1214            CheckStatus::Pass,
1215            "contract bytecode is present",
1216        ),
1217        Ok(_) => push_check(
1218            &mut checks,
1219            "contract code",
1220            CheckStatus::Fail,
1221            "no contract bytecode was found",
1222        ),
1223        Err(_) => push_check(
1224            &mut checks,
1225            "contract code",
1226            CheckStatus::Fail,
1227            "contract bytecode could not be read",
1228        ),
1229    }
1230
1231    if let Ok(asset) = asset {
1232        if asset.is_zero() {
1233            push_check(&mut checks, "asset()", CheckStatus::Fail, "returned the zero address");
1234        } else if asset == NATIVE_ASSET {
1235            push_check(
1236                &mut checks,
1237                "asset()",
1238                CheckStatus::Warn,
1239                "returned the ERC-7535 native-asset sentinel",
1240            );
1241        } else {
1242            push_check(&mut checks, "asset()", CheckStatus::Pass, format!("returned {asset}"));
1243            match provider.get_code_at(asset).block_id(block).await {
1244                Ok(code) if !code.is_empty() => push_check(
1245                    &mut checks,
1246                    "asset contract",
1247                    CheckStatus::Pass,
1248                    "underlying asset bytecode is present",
1249                ),
1250                Ok(_) => push_check(
1251                    &mut checks,
1252                    "asset contract",
1253                    CheckStatus::Warn,
1254                    "underlying asset has no bytecode and may be a system contract or precompile",
1255                ),
1256                Err(_) => push_check(
1257                    &mut checks,
1258                    "asset contract",
1259                    CheckStatus::Warn,
1260                    "underlying asset bytecode could not be read",
1261                ),
1262            }
1263            record_required(
1264                &mut checks,
1265                "asset balanceOf(address)",
1266                IERC20Metadata::new(asset, &provider).balanceOf(vault).block(block).call().await,
1267            );
1268        }
1269    } else {
1270        push_check(
1271            &mut checks,
1272            "asset()",
1273            CheckStatus::Fail,
1274            "call failed or returned incompatible data",
1275        );
1276    }
1277
1278    record_required(&mut checks, "totalAssets()", total_assets);
1279    record_required(&mut checks, "totalSupply()", total_supply);
1280    record_required(&mut checks, "balanceOf(address)", balance);
1281    record_required(&mut checks, "allowance(address,address)", allowance);
1282    record_zero_conversion(&mut checks, "convertToShares(0)", convert_to_shares);
1283    record_zero_conversion(&mut checks, "convertToAssets(0)", convert_to_assets);
1284    record_required(&mut checks, "maxDeposit(address)", max_deposit);
1285    record_preview(&mut checks, "previewDeposit(0)", "deposit", async_deposit, preview_deposit);
1286    record_required(&mut checks, "maxMint(address)", max_mint);
1287    record_preview(&mut checks, "previewMint(0)", "deposit", async_deposit, preview_mint);
1288    record_required(&mut checks, "maxWithdraw(address)", max_withdraw);
1289    record_preview(&mut checks, "previewWithdraw(0)", "redeem", async_redeem, preview_withdraw);
1290    record_required(&mut checks, "maxRedeem(address)", max_redeem);
1291    record_preview(&mut checks, "previewRedeem(0)", "redeem", async_redeem, preview_redeem);
1292    record_required(&mut checks, "name()", name);
1293    record_required(&mut checks, "symbol()", symbol);
1294    record_required(&mut checks, "decimals()", decimals);
1295
1296    let passed = checks.iter().filter(|check| matches!(check.status, CheckStatus::Pass)).count();
1297    let warnings = checks.iter().filter(|check| matches!(check.status, CheckStatus::Warn)).count();
1298    let failed = checks.iter().filter(|check| matches!(check.status, CheckStatus::Fail)).count();
1299    let report = CompatibilityReport {
1300        vault: vault.to_string(),
1301        account: account.to_string(),
1302        read_compatible: failed == 0,
1303        disclaimer: CHECK_DISCLAIMER,
1304        passed,
1305        warnings,
1306        failed,
1307        checks,
1308    };
1309    if failed > 0 {
1310        let message = format!("vault failed {failed} ERC-4626 compatibility probe(s)");
1311        if shell::is_json() {
1312            return Err(JsonError::new(
1313                report,
1314                JsonMessage::error("erc4626.compatibility_failed", message),
1315            )?
1316            .into());
1317        }
1318        print_compatibility_report(&report)?;
1319        eyre::bail!(message)
1320    }
1321    print_compatibility_report(&report)
1322}
1323
1324fn print_info(info: VaultInfo, human: bool, warnings: Vec<VaultWarning>) -> Result<()> {
1325    if shell::is_json() {
1326        return print_json_with_warnings(info, warnings);
1327    }
1328
1329    print_warnings(&warnings)?;
1330    let asset_symbol = info.asset_symbol.as_deref();
1331    let symbol = info.symbol.as_deref();
1332    print_field("Vault", &info.vault)?;
1333    print_field("Name", or_unavailable(info.name.as_ref()))?;
1334    print_field("Symbol", or_unavailable(symbol))?;
1335    print_field("Decimals", or_unavailable(info.decimals))?;
1336    print_field("Asset", &info.asset)?;
1337    print_field("Asset name", or_unavailable(info.asset_name.as_ref()))?;
1338    print_field("Asset symbol", or_unavailable(asset_symbol))?;
1339    print_field("Asset decimals", or_unavailable(info.asset_decimals))?;
1340    print_field("Total assets", display_amount(&info.total_assets, human, asset_symbol))?;
1341    print_field("Total supply", display_amount(&info.total_supply, human, symbol))?;
1342    print_field(
1343        "Assets per share",
1344        or_unavailable(
1345            info.assets_per_share.as_ref().map(|a| display_amount(a, human, asset_symbol)),
1346        ),
1347    )?;
1348    print_field(
1349        "Shares per asset",
1350        or_unavailable(info.shares_per_asset.as_ref().map(|a| display_amount(a, human, symbol))),
1351    )
1352}
1353
1354fn print_position(position: VaultPosition, human: bool, warnings: Vec<VaultWarning>) -> Result<()> {
1355    if shell::is_json() {
1356        return print_json_with_warnings(position, warnings);
1357    }
1358
1359    print_warnings(&warnings)?;
1360    let share_symbol = position.share_symbol.as_deref();
1361    let asset_symbol = position.asset_symbol.as_deref();
1362    print_field("Vault", &position.vault)?;
1363    print_field("Owner", &position.owner)?;
1364    print_field("Asset", &position.asset)?;
1365    print_field("Share symbol", or_unavailable(share_symbol))?;
1366    print_field("Share decimals", or_unavailable(position.share_decimals))?;
1367    print_field("Asset symbol", or_unavailable(asset_symbol))?;
1368    print_field("Asset decimals", or_unavailable(position.asset_decimals))?;
1369    print_field("Share balance", display_amount(&position.share_balance, human, share_symbol))?;
1370    print_field(
1371        "Assets equivalent",
1372        display_amount(&position.assets_equivalent, human, asset_symbol),
1373    )?;
1374    print_field("Max withdraw", display_amount(&position.max_withdraw, human, asset_symbol))?;
1375    print_field("Max redeem", display_amount(&position.max_redeem, human, share_symbol))
1376}
1377
1378fn print_compatibility_report(report: &CompatibilityReport) -> Result<()> {
1379    if shell::is_json() {
1380        return print_json_success(report);
1381    }
1382
1383    print_field("Vault", &report.vault)?;
1384    print_field("Account", &report.account)?;
1385    sh_println!("Note: {}", report.disclaimer)?;
1386    for check in &report.checks {
1387        let status = match check.status {
1388            CheckStatus::Pass => "PASS",
1389            CheckStatus::Warn => "WARN",
1390            CheckStatus::Fail => "FAIL",
1391        };
1392        sh_println!("{status:<4} {:<24} {}", check.name, check.detail)?;
1393    }
1394    sh_println!(
1395        "Summary: {} passed, {} warnings, {} failed",
1396        report.passed,
1397        report.warnings,
1398        report.failed
1399    )
1400}
1401
1402fn print_field(label: &str, value: impl std::fmt::Display) -> Result<()> {
1403    sh_println!("{label:<20} {value}")
1404}
1405
1406fn print_json_with_warnings<T: Serialize>(value: T, warnings: Vec<VaultWarning>) -> Result<()> {
1407    if warnings.is_empty() {
1408        print_json_success(value)
1409    } else {
1410        print_json_success_with_warnings(
1411            value,
1412            warnings
1413                .into_iter()
1414                .map(|warning| JsonMessage::warning(warning.code, warning.message))
1415                .collect(),
1416        )
1417    }
1418}
1419
1420fn print_warnings(warnings: &[VaultWarning]) -> Result<()> {
1421    for warning in warnings {
1422        sh_warn!("{}", warning.message)?;
1423    }
1424    Ok(())
1425}
1426
1427fn or_unavailable(value: Option<impl std::fmt::Display>) -> String {
1428    value.map_or_else(|| "<unavailable>".to_string(), |value| value.to_string())
1429}
1430
1431fn display_amount(amount: &TokenAmount, human: bool, symbol: Option<&str>) -> String {
1432    match (&amount.formatted, symbol.filter(|symbol| !symbol.is_empty())) {
1433        (Some(formatted), Some(symbol)) if human => format!("{formatted} {symbol}"),
1434        (Some(formatted), None) if human => formatted.clone(),
1435        _ => amount.raw.clone(),
1436    }
1437}
1438
1439/// `10^decimals`, or `None` when it overflows.
1440fn decimal_unit(decimals: u8) -> Option<U256> {
1441    U256::from(10).checked_pow(U256::from(decimals))
1442}
1443
1444fn push_check(
1445    checks: &mut Vec<CompatibilityCheck>,
1446    name: impl Into<String>,
1447    status: CheckStatus,
1448    detail: impl Into<String>,
1449) {
1450    checks.push(CompatibilityCheck { name: name.into(), status, detail: detail.into() });
1451}
1452
1453fn record_required<T, E>(
1454    checks: &mut Vec<CompatibilityCheck>,
1455    name: &str,
1456    result: std::result::Result<T, E>,
1457) {
1458    let (status, detail) = if result.is_ok() {
1459        (CheckStatus::Pass, "call succeeded")
1460    } else {
1461        (CheckStatus::Fail, "call failed or returned incompatible data")
1462    };
1463    push_check(checks, name, status, detail);
1464}
1465
1466fn record_zero_conversion<E>(
1467    checks: &mut Vec<CompatibilityCheck>,
1468    name: &str,
1469    result: std::result::Result<U256, E>,
1470) {
1471    match result {
1472        Ok(value) if value.is_zero() => {
1473            push_check(checks, name, CheckStatus::Pass, "returned zero")
1474        }
1475        Ok(value) => push_check(
1476            checks,
1477            name,
1478            CheckStatus::Warn,
1479            format!("returned {value}; zero input normally converts to zero"),
1480        ),
1481        Err(_) => {
1482            push_check(checks, name, CheckStatus::Fail, "call failed or returned incompatible data")
1483        }
1484    }
1485}
1486
1487fn record_preview<E>(
1488    checks: &mut Vec<CompatibilityCheck>,
1489    name: &str,
1490    request_kind: &str,
1491    async_supported: bool,
1492    result: std::result::Result<U256, E>,
1493) {
1494    match result {
1495        Ok(_) if async_supported => push_check(
1496            checks,
1497            name,
1498            CheckStatus::Warn,
1499            format!(
1500                "vault advertises asynchronous ERC-7540 {request_kind} support, which requires \
1501                 this preview to revert"
1502            ),
1503        ),
1504        Ok(value) if value.is_zero() => {
1505            push_check(checks, name, CheckStatus::Pass, "returned zero")
1506        }
1507        Ok(value) => push_check(
1508            checks,
1509            name,
1510            CheckStatus::Warn,
1511            format!("returned {value}; a zero-amount preview normally returns zero"),
1512        ),
1513        Err(_) if async_supported => push_check(
1514            checks,
1515            name,
1516            CheckStatus::Warn,
1517            format!(
1518                "reverted as required by advertised asynchronous ERC-7540 {request_kind} support"
1519            ),
1520        ),
1521        Err(_) => push_check(
1522            checks,
1523            name,
1524            CheckStatus::Fail,
1525            "call failed or returned incompatible data without advertised ERC-7540 support",
1526        ),
1527    }
1528}
1529
1530fn warn_if_zero_entry_max(method: &str, amount: U256) -> Result<()> {
1531    if amount.is_zero() {
1532        sh_warn!(
1533            "Vault reported zero from {method}; some ERC-4626 vaults intentionally return \
1534             conservative maxima or gate deposits, so this may not mean deposits are impossible."
1535        )?;
1536    }
1537    Ok(())
1538}
1539
1540fn warn_if_zero_exit_max(method: &str) -> Result<()> {
1541    sh_warn!("{}", zero_exit_warning(method).message)
1542}
1543
1544fn warn_if_native_asset(asset: Address) -> Result<()> {
1545    if asset == NATIVE_ASSET {
1546        sh_warn!("{}", native_asset_warning().message)?;
1547    }
1548    Ok(())
1549}
1550
1551fn zero_exit_warning(method: &str) -> VaultWarning {
1552    VaultWarning {
1553        code: match method {
1554            "maxWithdraw" => "erc4626_zero_max_withdraw",
1555            "maxRedeem" => "erc4626_zero_max_redeem",
1556            _ => "erc4626_zero_exit_max",
1557        },
1558        message: format!(
1559            "Vault reported zero from {method} even though the owner has shares; liquidity, \
1560             gates, withdrawal queues, or a conservative implementation may prevent the base \
1561             ERC-4626 exit."
1562        ),
1563    }
1564}
1565
1566fn native_asset_warning() -> VaultWarning {
1567    VaultWarning {
1568        code: "erc4626_native_asset",
1569        message: "Vault uses the ERC-7535 native-asset sentinel; base ERC-4626 write commands do \
1570                  not attach native value, so use `cast send --value` when the vault requires it."
1571            .to_string(),
1572    }
1573}
1574
1575/// Warns when the vault holds the native asset and resolves the write's account arguments.
1576async fn prepare_write<const N: usize>(
1577    vault: &NameOrAddress,
1578    accounts: [NameOrAddress; N],
1579    send_tx: &SendTxOpts,
1580) -> Result<[Address; N]> {
1581    let (provider, vault) = vault_at(&send_tx.eth.rpc, vault.clone()).await?;
1582    if let Ok(asset) = vault.asset().call().await {
1583        warn_if_native_asset(asset)?;
1584    }
1585
1586    let mut resolved = [Address::ZERO; N];
1587    for (slot, account) in resolved.iter_mut().zip(accounts) {
1588        *slot = account.resolve(&provider).await?;
1589    }
1590    Ok(resolved)
1591}
1592
1593async fn send_call<C: SolCall>(
1594    vault: NameOrAddress,
1595    call: C,
1596    send_tx: SendTxOpts,
1597    tx: TxParams,
1598) -> Result<()> {
1599    // Boxed to keep the large `cast send` future off this command's stack frame.
1600    Box::pin(SendTxArgs::contract_call(vault, call.abi_encode(), send_tx, tx).run()).await
1601}
1602
1603#[cfg(test)]
1604mod tests {
1605    use super::*;
1606    use clap::CommandFactory;
1607
1608    #[test]
1609    fn all_subcommands_document_example_output() {
1610        let command = Erc4626Subcommand::command();
1611        let subcommands = command.get_subcommands().collect::<Vec<_>>();
1612        assert_eq!(subcommands.len(), 19);
1613
1614        for subcommand in subcommands {
1615            let help = subcommand
1616                .get_long_about()
1617                .unwrap_or_else(|| panic!("{} is missing long help", subcommand.get_name()))
1618                .to_string();
1619            assert!(
1620                help.contains("Example:\n\n```text\n$ cast erc4626"),
1621                "{} is missing a fenced example command",
1622                subcommand.get_name()
1623            );
1624            assert!(
1625                help.contains("\n\nOutput:\n\n```text\n"),
1626                "{} is missing fenced example output",
1627                subcommand.get_name()
1628            );
1629        }
1630
1631        let info = command.find_subcommand("info").unwrap().get_long_about().unwrap().to_string();
1632        assert!(info.contains("--human"));
1633    }
1634}