Skip to content
Merged
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
54 changes: 53 additions & 1 deletion mainnet-contracts/src/CarrotVesting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ contract CarrotVesting is UUPSUpgradeable, Ownable2StepUpgradeable, PausableUpgr
error InvalidAmount();
error NoClaimableAmount();
error AlreadyDismantled();
error InvalidUpgradeVersion();

/**
* @notice Emitted when the vesting is initialized
Expand Down Expand Up @@ -140,6 +141,26 @@ contract CarrotVesting is UUPSUpgradeable, Ownable2StepUpgradeable, PausableUpgr
emit VestingReinitialized({ duration: newDuration, steps: newSteps });
}

/**
* @notice Reinitializes the vesting. This is used to change the duration or steps of the vesting after it has been initialized once.
* @dev This function can only be called by the owner
* @param newDuration2 The new duration of the vesting (seconds since the user deposits)
* @param newSteps2 The new number of steps in the vesting (Example: If the vesting is 6 months and the user can claim every month, steps = 6)
*/
function reinitializeVesting2(uint32 newDuration2, uint32 newSteps2) external onlyOwner reinitializer(4) {
require(newDuration2 > 0, InvalidDuration());
require(newSteps2 > 0, InvalidSteps());
require(newDuration2 >= newSteps2, InvalidDuration());
VestingStorage storage $ = _getCarrotVestingStorage();
require($.upgradeTimestamp != 0, InvalidUpgradeVersion());
require(!$.isDismantled, AlreadyDismantled());

$.newDuration2 = newDuration2;
$.newSteps2 = newSteps2;
$.upgradeTimestamp2 = uint48(block.timestamp);
emit VestingReinitialized({ duration: newDuration2, steps: newSteps2 });
}

/**
* @notice Deposits CARROT to burn them and start the vesting process to get PUFFER tokens in return
* @param amount The amount of CARROT to deposit
Expand Down Expand Up @@ -267,6 +288,15 @@ contract CarrotVesting is UUPSUpgradeable, Ownable2StepUpgradeable, PausableUpgr
return $.upgradeTimestamp;
}

/**
* @notice Gets the timestamp when the vesting was upgraded for the second time
* @return The timestamp when the vesting was upgraded for the second time
*/
function getUpgradeTimestamp2() external view returns (uint48) {
VestingStorage storage $ = _getCarrotVestingStorage();
return $.upgradeTimestamp2;
}

/**
* @notice Gets the duration of the vesting
* @return The duration of the vesting
Expand All @@ -285,6 +315,15 @@ contract CarrotVesting is UUPSUpgradeable, Ownable2StepUpgradeable, PausableUpgr
return $.newDuration;
}

/**
* @notice Gets the new duration of the vesting after the second upgrade
* @return The new duration of the vesting after the second upgrade
*/
function getNewDuration2() external view returns (uint32) {
VestingStorage storage $ = _getCarrotVestingStorage();
return $.newDuration2;
}

/**
* @notice Gets the steps of the vesting
* @return The steps of the vesting
Expand All @@ -303,6 +342,15 @@ contract CarrotVesting is UUPSUpgradeable, Ownable2StepUpgradeable, PausableUpgr
return $.newSteps;
}

/**
* @notice Gets the new steps of the vesting after the second upgrade
* @return The new steps of the vesting after the second upgrade
*/
function getNewSteps2() external view returns (uint32) {
VestingStorage storage $ = _getCarrotVestingStorage();
return $.newSteps2;
}

/**
* @notice Calculates the amount of PUFFER tokens that a user can claim at the current timestamp
* @dev For each vesting of the user, it calculates the number of steps that has passed since the user deposited and then calculates the amount
Expand Down Expand Up @@ -334,10 +382,14 @@ contract CarrotVesting is UUPSUpgradeable, Ownable2StepUpgradeable, PausableUpgr
// Vesting was created before the upgrade
duration = $.duration;
steps = $.steps;
} else {
} else if ($.upgradeTimestamp2 == 0 || vesting.depositedTimestamp < $.upgradeTimestamp2) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<= but not important tbh

// Vesting was created after the upgrade
duration = $.newDuration;
steps = $.newSteps;
} else {
// Vesting was created after the 2nd upgrade
duration = $.newDuration2;
steps = $.newSteps2;
}
uint256 endOfVesting = vesting.depositedTimestamp + duration;
if (vesting.lastClaimedTimestamp >= endOfVesting) {
Expand Down
3 changes: 3 additions & 0 deletions mainnet-contracts/src/CarrotVestingStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ abstract contract CarrotVestingStorage {
uint48 upgradeTimestamp;
uint32 newDuration;
uint32 newSteps;
uint48 upgradeTimestamp2;
uint32 newDuration2;
uint32 newSteps2;
}

// keccak256(abi.encode(uint256(keccak256("carrotvesting.storage")) - 1)) & ~bytes32(uint256(0xff))
Expand Down
166 changes: 166 additions & 0 deletions mainnet-contracts/test/unit/CarrotVesting.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ contract CarrotVestingTest is Test {
uint32 public constant STEPS = 1;
uint32 public constant NEW_DURATION = 6 * 30 days; // 6 months
uint32 public constant NEW_STEPS = 6;
uint32 public constant NEW_DURATION_2 = 12 * 30 days; // 12 months
uint32 public constant NEW_STEPS_2 = 12;
uint256 public constant TOTAL_PUFFER_REWARDS = 55_000_000 ether;
uint256 public constant MAX_CARROT_AMOUNT = 100_000_000 ether;
uint256 public constant EXCHANGE_RATE = 1e18 * TOTAL_PUFFER_REWARDS / MAX_CARROT_AMOUNT;
Expand Down Expand Up @@ -61,6 +63,11 @@ contract CarrotVestingTest is Test {
_;
}

modifier reinitialized() {
carrotVesting.reinitializeVesting(NEW_DURATION, NEW_STEPS);
_;
}

function test_constructor() public {
assertEq(address(carrotVesting.CARROT()), address(carrot), "CARROT address is not correct");
assertEq(address(carrotVesting.PUFFER()), address(puffer), "PUFFER address is not correct");
Expand Down Expand Up @@ -229,6 +236,165 @@ contract CarrotVestingTest is Test {
assertApproxEqAbs(totalClaimed, expectedTotal, 1, "New user total claim not correct");
}

function test_reinitializeVesting2_Unauthorized() public initialized reinitialized {
vm.startPrank(bob);
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, bob));
carrotVesting.reinitializeVesting2(365 days, 365);
vm.stopPrank();
}

function test_reinitializeVesting2_InvalidDuration() public initialized reinitialized {
vm.expectRevert(CarrotVesting.InvalidDuration.selector);
carrotVesting.reinitializeVesting2(0, 365);
}

function test_reinitializeVesting2_InvalidSteps() public initialized reinitialized {
vm.expectRevert(CarrotVesting.InvalidSteps.selector);
carrotVesting.reinitializeVesting2(365 days, 0);
}

function test_reinitializeVesting2_DurationLessThanSteps() public initialized reinitialized {
vm.expectRevert(CarrotVesting.InvalidDuration.selector);
carrotVesting.reinitializeVesting2(100, 200);
}

function test_reinitializeVesting2_CalledTwice() public initialized reinitialized {
carrotVesting.reinitializeVesting2(NEW_DURATION_2, NEW_STEPS_2);

vm.expectRevert(Initializable.InvalidInitialization.selector);
carrotVesting.reinitializeVesting2(730 days, 730);
}

function test_reinitializeVesting2_WhenDismantled() public initialized reinitialized {
carrotVesting.recoverPuffer(treasury);

vm.expectRevert(CarrotVesting.AlreadyDismantled.selector);
carrotVesting.reinitializeVesting2(NEW_DURATION_2, NEW_STEPS_2);
}

function test_reinitializeVesting2_NotReinitialized() public initialized {
// reinitializeVesting (the first upgrade) was never called, so upgradeTimestamp == 0
vm.expectRevert(CarrotVesting.InvalidUpgradeVersion.selector);
carrotVesting.reinitializeVesting2(NEW_DURATION_2, NEW_STEPS_2);
}

function test_reinitializeVesting2() public initialized reinitialized {
vm.expectEmit(true, true, true, true);
emit CarrotVesting.VestingReinitialized(NEW_DURATION_2, NEW_STEPS_2);
carrotVesting.reinitializeVesting2(NEW_DURATION_2, NEW_STEPS_2);

assertEq(carrotVesting.getNewDuration2(), NEW_DURATION_2, "Duration2 was not updated");
assertEq(carrotVesting.getNewSteps2(), NEW_STEPS_2, "Steps2 were not updated");
assertEq(carrotVesting.getUpgradeTimestamp2(), block.timestamp, "Upgrade timestamp2 was not set");
}

function test_reinitializeVesting2_AffectsExistingUsers() public initialized reinitialized {
uint256 depositAmount = 100 ether;
_startVesting(alice, depositAmount);

// Alice deposited after the first upgrade, so her vesting uses NEW_DURATION / NEW_STEPS.
// Fully vest and claim it before the second upgrade.
skip(NEW_DURATION);

uint256 expectedClaimableBeforeReinit = EXCHANGE_RATE * depositAmount / 1e18;
uint256 claimableBeforeReinit = carrotVesting.calculateClaimableAmount(alice);
assertApproxEqAbs(
claimableBeforeReinit, expectedClaimableBeforeReinit, 1, "Claimable before reinit2 not correct"
);

// Now reinitialize to the second-upgrade parameters
carrotVesting.reinitializeVesting2(NEW_DURATION_2, NEW_STEPS_2);

// Existing vesting should still be claimable after the second reinitialization
vm.startPrank(alice);
uint256 totalClaimableBeforeReinit = carrotVesting.claim();
vm.stopPrank();
assertApproxEqAbs(totalClaimableBeforeReinit, expectedClaimableBeforeReinit, 1, "Claim before reinit2 wrong");

uint256 depositAmount2 = 50 ether;
_startVesting(alice, depositAmount2);
uint256 secondVestingTimestamp = block.timestamp;

// The calculation should now use the NEW_DURATION_2 duration and steps
uint256 newStepDuration = NEW_DURATION_2 / NEW_STEPS_2;

skip(newStepDuration + 5);
uint256 numNewStepsPassed = (block.timestamp - secondVestingTimestamp) / newStepDuration;
uint256 expectedClaimableAfterReinit = (numNewStepsPassed * depositAmount2 / NEW_STEPS_2) * EXCHANGE_RATE / 1e18;
uint256 claimableAfterReinit = carrotVesting.calculateClaimableAmount(alice);

assertApproxEqAbs(claimableAfterReinit, expectedClaimableAfterReinit, 1, "Claimable after reinit2 not correct");

// Verify the vesting end time also changed to the new duration
vm.startPrank(alice);
skip(NEW_DURATION_2); // Skip the NEW_DURATION_2 full duration
uint256 totalClaimable = carrotVesting.claim();
vm.stopPrank();

uint256 expectedTotalClaimable = EXCHANGE_RATE * depositAmount2 / 1e18;
assertApproxEqAbs(totalClaimable, expectedTotalClaimable, 1, "Total claimable not correct");
}

function test_reinitializeVesting2_NewUsersUseNewParameters() public initialized reinitialized {
// Reinitialize to 12 months
carrotVesting.reinitializeVesting2(NEW_DURATION_2, NEW_STEPS_2);

// New user deposits
uint256 depositAmount = 100 ether;
_startVesting(alice, depositAmount);

uint256 newStepDuration = NEW_DURATION_2 / NEW_STEPS_2;

// Skip one new step
skip(newStepDuration);

uint256 expectedClaimable = (1 * depositAmount / NEW_STEPS_2) * EXCHANGE_RATE / 1e18;
uint256 claimable = carrotVesting.calculateClaimableAmount(alice);

assertApproxEqAbs(claimable, expectedClaimable, 1, "New user claimable not correct");

// Verify full vesting takes the new duration
skip(NEW_DURATION_2);

vm.startPrank(alice);
uint256 totalClaimed = carrotVesting.claim();
vm.stopPrank();

uint256 expectedTotal = EXCHANGE_RATE * depositAmount / 1e18;
assertApproxEqAbs(totalClaimed, expectedTotal, 1, "New user total claim not correct");
}

function test_reinitializeVesting2_MiddleWindowKeepsFirstUpgradeParams() public initialized reinitialized {
// Alice deposits AFTER the first upgrade but strictly BEFORE the second upgrade
uint256 depositAmount = 100 ether;
_startVesting(alice, depositAmount);
uint256 initTimestamp = block.timestamp;

// Second upgrade happens later, so Alice's depositedTimestamp < upgradeTimestamp2
skip(1 days);
carrotVesting.reinitializeVesting2(NEW_DURATION_2, NEW_STEPS_2);

// Alice's vesting must keep using the FIRST-upgrade parameters (NEW_DURATION / NEW_STEPS),
// not the second-upgrade parameters, and it must NOT become frozen.
uint256 stepDuration = NEW_DURATION / NEW_STEPS;
skip(stepDuration - 1 days);

uint256 expectedClaimable = EXCHANGE_RATE * depositAmount / NEW_STEPS / 1e18;
uint256 claimable = carrotVesting.calculateClaimableAmount(alice);
assertApproxEqAbs(claimable, expectedClaimable, 1, "Middle-window vesting should use first-upgrade params");

// Fully vests over NEW_DURATION (first upgrade), not NEW_DURATION_2
skip(NEW_DURATION);
vm.startPrank(alice);
uint256 totalClaimed = carrotVesting.claim();
vm.stopPrank();

uint256 expectedTotal = EXCHANGE_RATE * depositAmount / 1e18;
assertApproxEqAbs(totalClaimed, expectedTotal, 1, "Middle-window vesting total not correct");

_checkVesting(alice, 0, depositAmount, expectedTotal, block.timestamp, initTimestamp);
}

function test_startVesting_NotStarted() public {
vm.expectRevert(CarrotVesting.NotStarted.selector);
carrotVesting.startVesting(100 ether);
Expand Down