@@ -238,6 +238,141 @@ contract HypERC4626OwnerCollateralTest is HypTokenTest {
238238 );
239239 }
240240
241+ function testERC4626VaultDeposit_ceilingRounding_reservesMoreShares ()
242+ public
243+ {
244+ // This test verifies the mathematical difference between convertToShares (floor)
245+ // and previewWithdraw (ceiling) rounding when calculating shares for deposits.
246+
247+ uint256 transferAmount = 100e18 ;
248+ uint256 rewardAmount = 1e18 ;
249+
250+ // Setup: Transfer from Alice to Bob
251+ vm.prank (ALICE);
252+ primaryToken.approve (address (localToken), transferAmount);
253+ _performRemoteTransfer (0 , transferAmount);
254+
255+ // Add yield to the vault (increases share value)
256+ primaryToken.mintTo (address (vault), rewardAmount);
257+
258+ // Transfer back from Bob to Alice
259+ vm.prank (BOB);
260+ remoteToken.transferRemote (
261+ ORIGIN,
262+ BOB.addressToBytes32 (),
263+ transferAmount
264+ );
265+ _handleLocalTransfer (transferAmount);
266+
267+ // At this point, we have excess shares due to the yield
268+ uint256 totalShares = vault.maxRedeem (
269+ address (erc20CollateralVaultDeposit)
270+ );
271+ uint256 assetDeposited = erc20CollateralVaultDeposit.assetDeposited ();
272+
273+ // Calculate what convertToShares (floor rounding) would give us
274+ uint256 sharesFloor = vault.convertToShares (assetDeposited);
275+
276+ // Calculate what previewWithdraw (ceiling rounding) gives us
277+ uint256 sharesCeiling = vault.previewWithdraw (assetDeposited);
278+
279+ // When there's rounding involved, ceiling should be >= floor
280+ // and the excess shares should be: totalShares - sharesCeiling
281+ uint256 excessSharesWithCeiling = totalShares - sharesCeiling;
282+ uint256 excessSharesWithFloor = totalShares - sharesFloor;
283+
284+ // Verify the key difference: ceiling rounding calculates more shares to reserve
285+ // for the deposited assets, which means fewer excess shares to sweep
286+ assertLe (
287+ excessSharesWithCeiling,
288+ excessSharesWithFloor,
289+ "Ceiling rounding should reserve more shares for deposits "
290+ );
291+
292+ // Perform sweep and verify the amount swept is <= excessSharesWithFloor
293+ // Record logs to capture the event
294+ vm.recordLogs ();
295+ erc20CollateralVaultDeposit.sweep ();
296+
297+ // Get the logs and extract the ExcessSharesSwept event
298+ Vm.Log[] memory logs = vm.getRecordedLogs ();
299+ bool foundEvent = false ;
300+ uint256 sweptShares;
301+
302+ for (uint256 i = 0 ; i < logs.length ; i++ ) {
303+ // ExcessSharesSwept event signature: ExcessSharesSwept(uint256,uint256)
304+ if (
305+ logs[i].topics[0 ] ==
306+ keccak256 ("ExcessSharesSwept(uint256,uint256) " )
307+ ) {
308+ foundEvent = true ;
309+ // Decode the event data (amount is first parameter, assetsRedeemed is second)
310+ (sweptShares, ) = abi.decode (logs[i].data, (uint256 , uint256 ));
311+ break ;
312+ }
313+ }
314+
315+ assertTrue (
316+ foundEvent,
317+ "ExcessSharesSwept event should have been emitted "
318+ );
319+ assertLe (
320+ sweptShares,
321+ excessSharesWithFloor,
322+ "Swept amount should be <= excessSharesWithFloor "
323+ );
324+ }
325+
326+ function testERC4626VaultDeposit_sweep_usesCeilingRounding () public {
327+ // This test verifies that sweep() correctly sweeps excess shares after yield accrual
328+ // and leaves no shares behind when assetDeposited is 0.
329+
330+ uint256 transferAmount = 100e18 ;
331+ uint256 rewardAmount = 1e18 ;
332+
333+ // Setup: Transfer from Alice to Bob
334+ vm.prank (ALICE);
335+ primaryToken.approve (address (localToken), transferAmount);
336+ _performRemoteTransfer (0 , transferAmount);
337+
338+ // Add yield to the vault (increases share value)
339+ primaryToken.mintTo (address (vault), rewardAmount);
340+
341+ // Transfer back from Bob to Alice
342+ vm.prank (BOB);
343+ remoteToken.transferRemote (
344+ ORIGIN,
345+ BOB.addressToBytes32 (),
346+ transferAmount
347+ );
348+ _handleLocalTransfer (transferAmount);
349+
350+ uint256 ownerBalanceBefore = primaryToken.balanceOf (
351+ erc20CollateralVaultDeposit.owner ()
352+ );
353+
354+ // Call sweep() which should use previewWithdraw (ceiling rounding)
355+ erc20CollateralVaultDeposit.sweep ();
356+
357+ uint256 ownerBalanceAfter = primaryToken.balanceOf (
358+ erc20CollateralVaultDeposit.owner ()
359+ );
360+ uint256 sweptAmount = ownerBalanceAfter - ownerBalanceBefore;
361+
362+ // The swept amount should be positive (we did sweep excess shares)
363+ assertGt (sweptAmount, 0 , "Should have swept excess shares " );
364+
365+ // After sweep, we should have no shares remaining (assetDeposited is 0)
366+ uint256 remainingShares = vault.maxRedeem (
367+ address (erc20CollateralVaultDeposit)
368+ );
369+ assertEq (
370+ remainingShares,
371+ 0 ,
372+ "Should have no shares remaining after sweep with no deposits "
373+ );
374+ }
375+
241376 function testERC4626VaultDeposit_TransferFromSender_CorrectMetadata ()
242377 public
243378 {
0 commit comments