@@ -6,7 +6,8 @@ import { ReentrancyGuardTransient } from "@openzeppelin/contracts/utils/Reentran
66import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol " ;
77import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol " ;
88
9- import { WithdrawalRequest } from "../../interfaces/ICashModule.sol " ;
9+ import { IBridgeModule } from "../../interfaces/IBridgeModule.sol " ;
10+ import { SafeData, WithdrawalRequest } from "../../interfaces/ICashModule.sol " ;
1011import { IEtherFiSafe } from "../../interfaces/IEtherFiSafe.sol " ;
1112import { IFraxCustodian } from "../../interfaces/IFraxCustodian.sol " ;
1213import { IFraxRemoteHop, MessagingFee } from "../../interfaces/IFraxRemoteHop.sol " ;
@@ -19,7 +20,7 @@ import { ModuleCheckBalance } from "../ModuleCheckBalance.sol";
1920 * @notice Module for interacting with FraxUSD
2021 * @dev Extends ModuleBase to provide FraxUSD integration for Safes
2122 */
22- contract FraxModule is ModuleBase , ModuleCheckBalance , ReentrancyGuardTransient {
23+ contract FraxModule is ModuleBase , ModuleCheckBalance , ReentrancyGuardTransient , IBridgeModule {
2324 using MessageHashUtils for bytes32 ;
2425 using SafeCast for uint256 ;
2526
@@ -45,6 +46,10 @@ contract FraxModule is ModuleBase, ModuleCheckBalance, ReentrancyGuardTransient
4546 /// @notice Layerzero ethereum EID
4647 uint32 public constant ETHEREUM_EID = 30_101 ;
4748
49+ /// @notice Dust threshold for LayerZero OFT decimal conversion (1e12)
50+ /// @dev Amounts must be multiples of this value to avoid dust being locked
51+ uint256 public constant DUST_THRESHOLD = 1e12 ;
52+
4853 /// @notice TypeHash for deposit function signature
4954 bytes32 public constant DEPOSIT_SIG = keccak256 ("deposit " );
5055
@@ -54,6 +59,9 @@ contract FraxModule is ModuleBase, ModuleCheckBalance, ReentrancyGuardTransient
5459 /// @notice TypeHash for async withdraw function signature
5560 bytes32 public constant REQUEST_ASYNC_WITHDRAW_SIG = keccak256 ("requestAsyncWithdraw " );
5661
62+ /// @notice TypeHash for cancel async withdraw function signature
63+ bytes32 public constant CANCEL_ASYNC_WITHDRAW_SIG = keccak256 ("cancelAsyncWithdraw " );
64+
5765 /// @notice Cross-chain withdrawal requests for each Safe
5866 mapping (address safe = > AsyncWithdrawal withdrawal ) private withdrawals;
5967
@@ -69,6 +77,18 @@ contract FraxModule is ModuleBase, ModuleCheckBalance, ReentrancyGuardTransient
6977 /// @notice Error thrown when no matching withdrawal is found for the safe
7078 error CannotFindMatchingWithdrawalForSafe ();
7179
80+ /// @notice Thrown when a caller lacks the proper authorization for an operation
81+ error Unauthorized ();
82+
83+ /// @notice Error for Invalid Owner quorum signatures
84+ error InvalidSignatures ();
85+
86+ /// @notice Error thrown when withdrawal amount contains dust (not a multiple of DUST_THRESHOLD)
87+ error AmountContainsDust ();
88+
89+ /// @notice Error thrown when custodian has insufficient balance for synchronous deposit
90+ error InsufficientCustodianBalance ();
91+
7292 /// @notice Emitted when safe deposits USDC into Frax USD
7393 event Deposit (address indexed safe , address indexed inputToken , uint256 inputAmount , uint256 outputAmount );
7494
@@ -81,6 +101,9 @@ contract FraxModule is ModuleBase, ModuleCheckBalance, ReentrancyGuardTransient
81101 /// @notice Emitted when safe executes an async withdrawal from FraxUSD
82102 event AsyncWithdrawalExecuted (address indexed safe , uint256 amountToWithdraw , uint32 dstEid , address to );
83103
104+ /// @notice Emitted when an async withdrawal request is cancelled
105+ event AsyncWithdrawalCancelled (address indexed safe , uint256 amountToWithdraw , uint32 dstEid , address to );
106+
84107 /**
85108 * @notice Contract constructor
86109 * @param _etherFiDataProvider Address of the EtherFiDataProvider contract
@@ -91,7 +114,7 @@ contract FraxModule is ModuleBase, ModuleCheckBalance, ReentrancyGuardTransient
91114 * @custom:throws InvalidInput If any provided address is zero
92115 */
93116 constructor (address _etherFiDataProvider , address _fraxusd , address _custodian , address _remoteHop ) ModuleBase (_etherFiDataProvider) ModuleCheckBalance (_etherFiDataProvider) {
94- if (_etherFiDataProvider == address (0 ) || _fraxusd == address (0 ) || _remoteHop == address (0 )) revert InvalidInput ();
117+ if (_etherFiDataProvider == address (0 ) || _fraxusd == address (0 ) || _custodian == address ( 0 ) || _remoteHop == address (0 )) revert InvalidInput ();
95118
96119 fraxusd = _fraxusd;
97120 custodian = _custodian;
@@ -134,13 +157,20 @@ contract FraxModule is ModuleBase, ModuleCheckBalance, ReentrancyGuardTransient
134157 * @param amountToDeposit The amount of asset tokens to deposit
135158 * @param minReturnAmount The minimum amount of asset to return (18 decimals - FraxUSD)
136159 * @custom:throws InvalidInput If amount or min return is zero
160+ * @custom:throws InsufficientCustodianBalance If custodian doesn't have enough balance for synchronous deposit
137161 * @custom:throws InsufficientReturnAmount If the FraxUSD received is less than expected
138162 */
139163 function _deposit (address safe , address assetToDeposit , uint256 amountToDeposit , uint256 minReturnAmount ) internal {
140164 if (amountToDeposit == 0 || assetToDeposit == address (0 )) revert InvalidInput ();
141165
142166 _checkAmountAvailable (safe, assetToDeposit, amountToDeposit);
143167
168+ // Validate that custodian has sufficient balance for synchronous deposit
169+ // The custodian needs at least minReturnAmount of fraxusd tokens to fulfill the deposit synchronously
170+ // If it doesn't have enough, it will attempt a cross-chain mint which requires native fees and is async
171+ uint256 custodianBalance = ERC20 (fraxusd).balanceOf (custodian);
172+ if (custodianBalance < minReturnAmount) revert InsufficientCustodianBalance ();
173+
144174 address [] memory to = new address [](2 );
145175 bytes [] memory data = new bytes [](2 );
146176 uint256 [] memory values = new uint256 [](2 );
@@ -282,6 +312,44 @@ contract FraxModule is ModuleBase, ModuleCheckBalance, ReentrancyGuardTransient
282312 delete withdrawals[safe];
283313 }
284314
315+ /**
316+ * @notice Cancels an async withdrawal request for a safe
317+ * @param safe Address of the EtherFiSafe
318+ * @param signers Array of addresses of safe owners that signed the transaction
319+ * @param signatures Array of signatures from the signers
320+ */
321+ function cancelAsyncWithdraw (address safe , address [] calldata signers , bytes [] calldata signatures ) external nonReentrant onlyEtherFiSafe (safe) {
322+ _checkCancelAsyncWithdrawSignature (safe, signers, signatures);
323+
324+ AsyncWithdrawal memory withdrawal = withdrawals[safe];
325+ if (withdrawal.recipient == address (0 )) revert NoAsyncWithdrawalQueued ();
326+
327+ SafeData memory data = cashModule.getData (safe);
328+ // If there is a withdrawal pending from this module on Cash Module, cancel it
329+ if (data.pendingWithdrawalRequest.recipient == address (this )) cashModule.cancelWithdrawalByModule (safe);
330+
331+ if (withdrawal.recipient != address (0 )) {
332+ emit AsyncWithdrawalCancelled (safe, withdrawal.amount, ETHEREUM_EID, withdrawal.recipient);
333+ delete withdrawals[safe];
334+ }
335+ }
336+
337+ /**
338+ * @notice Cancels an async withdrawal request by the cash module
339+ * @dev This function is intended to be called by the cash module to cancel an async withdrawal
340+ * @param safe Address of the EtherFiSafe
341+ */
342+ function cancelBridgeByCashModule (address safe ) external {
343+ if (msg .sender != etherFiDataProvider.getCashModule ()) revert Unauthorized ();
344+
345+ AsyncWithdrawal memory withdrawal = withdrawals[safe];
346+ // Return if no withdrawal found for Frax
347+ if (withdrawal.recipient == address (0 )) return ;
348+
349+ emit AsyncWithdrawalCancelled (safe, withdrawal.amount, ETHEREUM_EID, withdrawal.recipient);
350+ delete withdrawals[safe];
351+ }
352+
285353 /**
286354 * @dev Creates a digest hash for the async withdraw operation
287355 * @param _recipient Recipient address from Frax api
@@ -292,15 +360,29 @@ contract FraxModule is ModuleBase, ModuleCheckBalance, ReentrancyGuardTransient
292360 return keccak256 (abi.encodePacked (REQUEST_ASYNC_WITHDRAW_SIG, block .chainid , address (this ), _useNonce (safe), safe, abi.encode (fraxusd, _recipient, _withdrawAmount))).toEthSignedMessageHash ();
293361 }
294362
363+ /**
364+ * @dev Verifies that the transaction has been properly signed by the required signers
365+ * @param safe Address of the EtherFiSafe
366+ * @param signers Array of addresses that signed the transaction
367+ * @param signatures Array of signatures from the signers
368+ * @custom:throws InvalidSignatures if the signatures are invalid
369+ */
370+ function _checkCancelAsyncWithdrawSignature (address safe , address [] calldata signers , bytes [] calldata signatures ) internal {
371+ bytes32 digestHash = keccak256 (abi.encodePacked (CANCEL_ASYNC_WITHDRAW_SIG, block .chainid , address (this ), IEtherFiSafe (safe).useNonce (), safe)).toEthSignedMessageHash ();
372+ if (! IEtherFiSafe (safe).checkSignatures (digestHash, signers, signatures)) revert InvalidSignatures ();
373+ }
374+
295375 /**
296376 * @notice Function to request an async withdrawal
297377 * @param safe Address for user safe
298378 * @param _recipient Recipient address from Frax api
299379 * @param _withdrawAmount Amount to withdraw asynchronously
300- * @custom:throws InvalidInput If the amount is zero
380+ * @custom:throws InvalidInput If the amount is zero or recipient is zero
381+ * @custom:throws AmountContainsDust If the amount is not a multiple of DUST_THRESHOLD
301382 */
302383 function _requestAsyncWithdraw (address safe , address _recipient , uint256 _withdrawAmount ) internal {
303384 if (_withdrawAmount == 0 || _recipient == address (0 )) revert InvalidInput ();
385+ if (_withdrawAmount % DUST_THRESHOLD != 0 ) revert AmountContainsDust ();
304386
305387 cashModule.requestWithdrawalByModule (safe, fraxusd, _withdrawAmount);
306388
0 commit comments