Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/StakedUSX.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ contract StakedUSX is ERC4626Upgradeable, UUPSUpgradeable, ReentrancyGuardUpgrad
/*=========================== Constants =========================*/

/// @dev Minimum epoch duration in seconds
uint256 private constant MIN_EPOCH_DURATION = 1 days;
uint256 private constant MIN_EPOCH_DURATION = 1 hours;

/// @dev Precision for the fee fractions
uint256 private constant FEE_PRECISION = 1000000;
Expand Down Expand Up @@ -223,6 +223,18 @@ contract StakedUSX is ERC4626Upgradeable, UUPSUpgradeable, ReentrancyGuardUpgrad
return Math.mulDiv(withdrawalAmount, $.withdrawalFeeFraction, FEE_PRECISION, Math.Rounding.Floor);
}

/// @notice Returns true if deposits are open, false otherwise
function isDepositOpen() public view returns (bool) {
// For every two weeks (in UTC), only the first Monday is allowed to deposit
// 345600 seconds = 4 days offset to align with Monday 00:00:00 UTC
uint256 epoch = (block.timestamp - 345600) / (14 days);
uint256 epochStartTime = epoch * (14 days) + 345600;
if (block.timestamp < epochStartTime || block.timestamp >= epochStartTime + 1 days) {
return false;
}
return true;
}

/*=========================== Governance Functions =========================*/

/// @notice Sets withdrawal fee with precision to 0.001 percent
Expand Down Expand Up @@ -320,6 +332,11 @@ contract StakedUSX is ERC4626Upgradeable, UUPSUpgradeable, ReentrancyGuardUpgrad
// Check if deposits are frozen
if ($.depositPaused) revert DepositsPaused();

// Check if deposits are open
if (!isDepositOpen()) {
revert DepositsPaused();
}

// Call parent implementation
super._deposit(caller, receiver, assets, shares);
}
Expand Down
116 changes: 113 additions & 3 deletions test/StakedUSX.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ contract StakedUSXTest is LocalDeployTestSetup {
deal(address(usdc), user2, 1_000_000e6);
vm.prank(user2);
usdc.approve(address(usx), type(uint256).max);

vm.warp(345600);
}

/* =========================== Helpers =========================== */
Expand Down Expand Up @@ -142,7 +144,7 @@ contract StakedUSXTest is LocalDeployTestSetup {
vm.expectRevert(StakedUSX.InvalidWithdrawalFeeFraction.selector);
susx.setWithdrawalFeeFraction(20001);

// Only admin can set epoch duration; must be >= 1 day
// Only admin can set epoch duration; must be >= 1 hour
vm.expectRevert(StakedUSX.NotAdmin.selector);
susx.setEpochDuration(2 days);
vm.prank(admin);
Expand All @@ -152,7 +154,7 @@ contract StakedUSXTest is LocalDeployTestSetup {
assertEq(susx.epochDuration(), 2 days);
vm.prank(admin);
vm.expectRevert(StakedUSX.InvalidEpochDuration.selector);
susx.setEpochDuration(12 hours);
susx.setEpochDuration(1 hours - 1);

// setGovernance onlyGovernance and non-zero
vm.expectRevert(StakedUSX.NotGovernance.selector);
Expand Down Expand Up @@ -251,6 +253,114 @@ contract StakedUSXTest is LocalDeployTestSetup {
vm.stopPrank();
}

/* =========================== Deposit Window Tests =========================== */

function test_isDepositOpen_at_epoch_start_returns_true() public {
// setUp warps to 345600 (Monday 00:00:00 UTC, epoch 0 start)
assertTrue(susx.isDepositOpen());
}

function test_isDepositOpen_within_window_returns_true() public {
// At epoch start + 12 hours (still within 24h window)
vm.warp(345600 + 12 hours);
assertTrue(susx.isDepositOpen());

// At epoch start + 23 hours 59 minutes (still within window)
vm.warp(345600 + 23 hours + 59 minutes);
assertTrue(susx.isDepositOpen());
}

function test_isDepositOpen_before_epoch_start_returns_false() public {
// Before epoch 0 start
vm.warp(345600+86400*7*2 - 1);
assertFalse(susx.isDepositOpen());

// 1 day before epoch start
vm.warp(345600+86400*7*2 - 1 days);
assertFalse(susx.isDepositOpen());
}

function test_isDepositOpen_after_window_returns_false() public {
// Exactly at epoch start + 1 day (window closed)
vm.warp(345600 + 1 days);
assertFalse(susx.isDepositOpen());

// After window closes
vm.warp(345600 + 1 days + 1);
assertFalse(susx.isDepositOpen());

// Mid-epoch (e.g., 7 days into epoch)
vm.warp(345600 + 7 days);
assertFalse(susx.isDepositOpen());
}

function test_isDepositOpen_next_epoch_window_returns_true() public {
// Epoch 1 start (14 days after epoch 0)
uint256 epoch1Start = 345600 + 14 days;
vm.warp(epoch1Start);
assertTrue(susx.isDepositOpen());

// Within epoch 1 window
vm.warp(epoch1Start + 12 hours);
assertTrue(susx.isDepositOpen());

// After epoch 1 window
vm.warp(epoch1Start + 1 days);
assertFalse(susx.isDepositOpen());
}

function test_isDepositOpen_multiple_epochs() public {
// Test epoch 2
uint256 epoch2Start = 345600 + 28 days; // 2 * 14 days
vm.warp(epoch2Start);
assertTrue(susx.isDepositOpen());

vm.warp(epoch2Start + 1 days);
assertFalse(susx.isDepositOpen());

// Test epoch 3
uint256 epoch3Start = 345600 + 42 days; // 3 * 14 days
vm.warp(epoch3Start);
assertTrue(susx.isDepositOpen());

vm.warp(epoch3Start + 12 hours);
assertTrue(susx.isDepositOpen());
}

function test_deposit_succeeds_when_isDepositOpen_returns_true() public {
_mintUSXTo(user, 1000e6);
vm.startPrank(user);
usx.approve(address(susx), type(uint256).max);

// At epoch start (isDepositOpen should be true)
vm.warp(345600);
assertTrue(susx.isDepositOpen());
susx.deposit(1000e18, user);
assertEq(susx.balanceOf(user), 1000e18);

vm.stopPrank();
}

function test_deposit_reverts_when_isDepositOpen_returns_false() public {
_mintUSXTo(user, 1000e6);
vm.startPrank(user);
usx.approve(address(susx), type(uint256).max);

// After window closes (isDepositOpen should be false)
vm.warp(345600 + 1 days);
assertFalse(susx.isDepositOpen());
vm.expectRevert(StakedUSX.DepositsPaused.selector);
susx.deposit(1000e18, user);

// Mid-epoch
vm.warp(345600 + 7 days);
assertFalse(susx.isDepositOpen());
vm.expectRevert(StakedUSX.DepositsPaused.selector);
susx.deposit(1000e18, user);

vm.stopPrank();
}

/* =========================== Withdrawals & Claims =========================== */

function test_redeem_creates_withdrawal_request_and_updates_state() public {
Expand Down Expand Up @@ -442,7 +552,7 @@ contract StakedUSXTest is LocalDeployTestSetup {
usx.mintUSX(address(susx), rewardAmount);
vm.prank(treasuryProxy);
susx.notifyRewards(rewardAmount);
vm.warp(block.timestamp + susx.epochDuration() / 3);
vm.warp(block.timestamp + 86400*7*2);

// New depositor should mint fewer shares than assets due to price > 1
uint256 depositAssets = 300e18;
Expand Down
Loading