From 15053301954d173cae76a33b1032b3eef91a7702 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 5 Feb 2024 22:29:25 +0400 Subject: [PATCH] Abstract wrapped/unwrapped status of derivative tokens away from the user --- src/modules/Derivative.sol | 26 +- src/modules/derivatives/LinearVesting.sol | 94 +-- test/modules/derivatives/LinearVesting.t.sol | 768 +++++------------- .../LinearVestingIntegration.t.sol | 8 +- .../mocks/MockDerivativeModule.sol | 12 +- 5 files changed, 254 insertions(+), 654 deletions(-) diff --git a/src/modules/Derivative.sol b/src/modules/Derivative.sol index d3ac33e5..5a302014 100644 --- a/src/modules/Derivative.sol +++ b/src/modules/Derivative.sol @@ -86,34 +86,30 @@ abstract contract Derivative { bool wrapped_ ) external virtual returns (uint256, address, uint256); - function redeemMax(uint256 tokenId_, bool wrapped_) external virtual; + /// @notice Redeem all available derivative tokens for underlying collateral + /// + /// @param tokenId_ The ID of the derivative token to redeem + function redeemMax(uint256 tokenId_) external virtual; /// @notice Redeem derivative tokens for underlying collateral /// /// @param tokenId_ The ID of the derivative token to redeem /// @param amount_ The amount of derivative tokens to redeem - /// @param wrapped_ Whether (true) or not (false) to redeem wrapped ERC20 derivative tokens - function redeem(uint256 tokenId_, uint256 amount_, bool wrapped_) external virtual; + function redeem(uint256 tokenId_, uint256 amount_) external virtual; /// @notice Determines the amount of redeemable tokens for a given derivative token /// /// @param owner_ The owner of the derivative token /// @param tokenId_ The ID of the derivative token - /// @param wrapped_ Whether (true) or not (false) to redeem wrapped ERC20 derivative tokens /// @return amount_ The amount of redeemable tokens - function redeemable( - address owner_, - uint256 tokenId_, - bool wrapped_ - ) external view virtual returns (uint256); + function redeemable(address owner_, uint256 tokenId_) external view virtual returns (uint256); /// @notice Exercise a conversion of the derivative token per the specific implementation logic /// @dev Used for options or other derivatives with convertible options, e.g. Rage vesting. /// /// @param tokenId_ The ID of the derivative token to exercise /// @param amount The amount of derivative tokens to exercise - /// @param wrapped_ Whether (true) or not (false) to exercise wrapped ERC20 derivative tokens - function exercise(uint256 tokenId_, uint256 amount, bool wrapped_) external virtual; + function exercise(uint256 tokenId_, uint256 amount) external virtual; /// @notice Reclaim posted collateral for a derivative token which can no longer be exercised /// @notice Access controlled: only callable by the derivative issuer via the auction house. @@ -127,13 +123,7 @@ abstract contract Derivative { /// @param tokenId_ The ID of the derivative token to transform /// @param from_ The address of the owner of the derivative token /// @param amount_ The amount of derivative tokens to transform - /// @param wrapped_ Whether (true) or not (false) to transform wrapped ERC20 derivative tokens - function transform( - uint256 tokenId_, - address from_, - uint256 amount_, - bool wrapped_ - ) external virtual; + function transform(uint256 tokenId_, address from_, uint256 amount_) external virtual; /// @notice Wrap an existing derivative into an ERC20 token for composability /// Deploys the ERC20 wrapper if it does not already exist diff --git a/src/modules/derivatives/LinearVesting.sol b/src/modules/derivatives/LinearVesting.sol index fdebb5b8..66b31458 100644 --- a/src/modules/derivatives/LinearVesting.sol +++ b/src/modules/derivatives/LinearVesting.sol @@ -7,6 +7,7 @@ import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {ClonesWithImmutableArgs} from "src/lib/clones/ClonesWithImmutableArgs.sol"; import {Timestamp} from "src/lib/Timestamp.sol"; import {ERC6909Metadata} from "src/lib/ERC6909Metadata.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {Derivative, DerivativeModule} from "src/modules/Derivative.sol"; import {Module, Veecode, toKeycode, wrapVeecode} from "src/modules/Modules.sol"; @@ -16,6 +17,7 @@ contract LinearVesting is DerivativeModule { using SafeTransferLib for ERC20; using ClonesWithImmutableArgs for address; using Timestamp for uint48; + using FixedPointMathLib for uint256; // ========== EVENTS ========== // @@ -72,8 +74,7 @@ contract LinearVesting is DerivativeModule { address internal immutable _IMPLEMENTATION; /// @notice Stores the amount of tokens that have been claimed for a particular token id and owner - mapping(address owner => mapping(uint256 tokenId => mapping(bool wrapped => uint256))) internal - claimed; + mapping(address owner_ => mapping(uint256 tokenId_ => uint256 claimedAmount_)) internal claimed; // ========== MODULE SETUP ========== // @@ -297,23 +298,31 @@ contract LinearVesting is DerivativeModule { /// /// @param tokenId_ The ID of the derivative token /// @param amount_ The amount of the derivative token to redeem - /// @param wrapped_ Whether or not to redeem wrapped derivative tokens - function _redeem(uint256 tokenId_, uint256 amount_, bool wrapped_) internal { - // Update claimed amount - claimed[msg.sender][tokenId_][wrapped_] += amount_; - + function _redeem(uint256 tokenId_, uint256 amount_) internal { Token storage tokenData = tokenMetadata[tokenId_]; - // Burn the derivative token - if (wrapped_ == false) { - _burn(msg.sender, tokenId_, amount_); + // Get the balances of the tokens + uint256 derivativeBalance = balanceOf[msg.sender][tokenId_]; + uint256 wrappedBalance = tokenData.wrapped == address(0) + ? 0 + : SoulboundCloneERC20(tokenData.wrapped).balanceOf(msg.sender); + + uint256 derivativeToBurn = amount_ > derivativeBalance ? derivativeBalance : amount_; + uint256 wrappedToBurn = amount_ - derivativeToBurn; + if (wrappedToBurn > wrappedBalance) { + revert InsufficientBalance(); } - // Burn the wrapped derivative token - else { - if (tokenData.wrapped == address(0)) revert InvalidParams(); - SoulboundCloneERC20 wrappedToken = SoulboundCloneERC20(tokenData.wrapped); - wrappedToken.burn(msg.sender, amount_); + // Update claimed amount + claimed[msg.sender][tokenId_] += amount_; + + // Burn the unwrapped tokens + if (derivativeToBurn > 0) { + _burn(msg.sender, tokenId_, derivativeToBurn); + } + // Burn the wrapped tokens - will be 0 if not wrapped + if (wrappedToBurn > 0) { + SoulboundCloneERC20(tokenData.wrapped).burn(msg.sender, wrappedToBurn); } // Transfer the underlying token to the owner @@ -325,18 +334,15 @@ contract LinearVesting is DerivativeModule { } /// @inheritdoc Derivative - function redeemMax( - uint256 tokenId_, - bool wrapped_ - ) external virtual override onlyValidTokenId(tokenId_) { + function redeemMax(uint256 tokenId_) external virtual override onlyValidTokenId(tokenId_) { // Determine the redeemable amount - uint256 redeemableAmount = redeemable(msg.sender, tokenId_, wrapped_); + uint256 redeemableAmount = redeemable(msg.sender, tokenId_); // If the redeemable amount is 0, revert if (redeemableAmount == 0) revert InsufficientBalance(); // Redeem the tokens - _redeem(tokenId_, redeemableAmount, wrapped_); + _redeem(tokenId_, redeemableAmount); } /// @inheritdoc Derivative @@ -346,19 +352,18 @@ contract LinearVesting is DerivativeModule { /// - The derivative token with `tokenId_` has not been deployed function redeem( uint256 tokenId_, - uint256 amount_, - bool wrapped_ + uint256 amount_ ) external virtual override onlyValidTokenId(tokenId_) { if (amount_ == 0) revert InvalidParams(); // Get the redeemable amount - uint256 redeemableAmount = redeemable(msg.sender, tokenId_, wrapped_); + uint256 redeemableAmount = redeemable(msg.sender, tokenId_); // If the redeemable amount is less than the requested amount, revert if (redeemableAmount < amount_) revert InsufficientBalance(); // Redeem the tokens - _redeem(tokenId_, amount_, wrapped_); + _redeem(tokenId_, amount_); } /// @notice Returns the amount of vested tokens that can be redeemed for the underlying base token @@ -376,8 +381,7 @@ contract LinearVesting is DerivativeModule { /// @return uint256 The amount of tokens that can be redeemed function redeemable( address owner_, - uint256 tokenId_, - bool wrapped_ + uint256 tokenId_ ) public view virtual override onlyValidTokenId(tokenId_) returns (uint256) { // Get the vesting data Token storage token = tokenMetadata[tokenId_]; @@ -386,18 +390,12 @@ contract LinearVesting is DerivativeModule { // If before the start time, 0 if (block.timestamp <= data.start) return 0; - // Handle the case where the wrapped derivative is not deployed - if (wrapped_ == true && token.wrapped == address(0)) return 0; - // Get balances - uint256 derivativeBalance; - if (wrapped_) { - derivativeBalance = SoulboundCloneERC20(token.wrapped).balanceOf(owner_); - } else { - derivativeBalance = balanceOf[owner_][tokenId_]; - } - uint256 claimedBalance = claimed[owner_][tokenId_][wrapped_]; - uint256 totalAmount = derivativeBalance + claimedBalance; + uint256 derivativeBalance = balanceOf[owner_][tokenId_]; + uint256 wrappedBalance = + token.wrapped == address(0) ? 0 : SoulboundCloneERC20(token.wrapped).balanceOf(owner_); + uint256 claimedBalance = claimed[owner_][tokenId_]; + uint256 totalAmount = derivativeBalance + wrappedBalance + claimedBalance; // Determine the amount that has been vested until date, excluding what has already been claimed uint256 vested; @@ -407,7 +405,7 @@ contract LinearVesting is DerivativeModule { } // If before the expiry time, calculate what has vested already else { - vested = (totalAmount * (block.timestamp - data.start)) / (data.expiry - data.start); + vested = totalAmount.mulDivDown(block.timestamp - data.start, data.expiry - data.start); } // Check invariant: cannot have claimed more than vested @@ -423,7 +421,7 @@ contract LinearVesting is DerivativeModule { /// @inheritdoc Derivative /// @dev Not implemented - function exercise(uint256, uint256, bool) external virtual override { + function exercise(uint256, uint256) external virtual override { revert Derivative.Derivative_NotImplemented(); } @@ -435,7 +433,7 @@ contract LinearVesting is DerivativeModule { /// @inheritdoc Derivative /// @dev Not implemented - function transform(uint256, address, uint256, bool) external virtual override { + function transform(uint256, address, uint256) external virtual override { revert Derivative.Derivative_NotImplemented(); } @@ -449,6 +447,8 @@ contract LinearVesting is DerivativeModule { ) external virtual override onlyValidTokenId(tokenId_) { if (amount_ == 0) revert InvalidParams(); + if (balanceOf[msg.sender][tokenId_] < amount_) revert InsufficientBalance(); + // Burn the derivative token _burn(msg.sender, tokenId_, amount_); @@ -459,8 +459,6 @@ contract LinearVesting is DerivativeModule { // Mint the wrapped derivative SoulboundCloneERC20(token.wrapped).mint(msg.sender, amount_); - // TODO adjust claimed - emit Wrapped(tokenId_, msg.sender, amount_, token.wrapped); } @@ -475,15 +473,17 @@ contract LinearVesting is DerivativeModule { ) external virtual override onlyValidTokenId(tokenId_) onlyDeployedWrapped(tokenId_) { if (amount_ == 0) revert InvalidParams(); - // Burn the wrapped derivative token Token storage token = tokenMetadata[tokenId_]; - SoulboundCloneERC20(token.wrapped).burn(msg.sender, amount_); + SoulboundCloneERC20 wrappedToken = SoulboundCloneERC20(token.wrapped); + + if (wrappedToken.balanceOf(msg.sender) < amount_) revert InsufficientBalance(); + + // Burn the wrapped derivative token + wrappedToken.burn(msg.sender, amount_); // Mint the derivative token _mint(msg.sender, tokenId_, amount_); - // TODO adjust claimed - emit Unwrapped(tokenId_, msg.sender, amount_, token.wrapped); } diff --git a/test/modules/derivatives/LinearVesting.t.sol b/test/modules/derivatives/LinearVesting.t.sol index 88f68bed..8f0000ae 100644 --- a/test/modules/derivatives/LinearVesting.t.sol +++ b/test/modules/derivatives/LinearVesting.t.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.19; // Libraries import {Test} from "forge-std/Test.sol"; -import {console2} from "forge-std/console2.sol"; import {Permit2User} from "test/lib/permit2/Permit2User.sol"; import {StringHelper} from "test/lib/String.sol"; @@ -1133,7 +1132,7 @@ contract LinearVestingTest is Test, Permit2User { // Call vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, AMOUNT, false); + linearVesting.redeem(derivativeTokenId, AMOUNT); } function test_redeem_givenRedeemAmountIsZero_reverts() public givenDerivativeIsDeployed { @@ -1143,7 +1142,7 @@ contract LinearVestingTest is Test, Permit2User { // Call vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, 0, false); + linearVesting.redeem(derivativeTokenId, 0); } function test_redeem_givenAmountGreaterThanRedeemable_reverts(uint48 elapsed_) @@ -1161,7 +1160,7 @@ contract LinearVestingTest is Test, Permit2User { // Call vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, AMOUNT, false); + linearVesting.redeem(derivativeTokenId, AMOUNT); } function test_redeem_insufficientBalance_reverts() @@ -1174,24 +1173,27 @@ contract LinearVestingTest is Test, Permit2User { vm.expectRevert(err); // Call - linearVesting.redeem(derivativeTokenId, AMOUNT, false); + linearVesting.redeem(derivativeTokenId, AMOUNT); } - function test_redeem_wrapped_givenWrappedTokenNotDeployed() + function test_redeem_givenWrappedTokenNotDeployed(uint256 amount_) public givenDerivativeIsDeployed givenAliceHasDerivativeTokens(AMOUNT) + givenAfterVestingExpiry { - // Expect revert - bytes memory err = abi.encodeWithSelector(LinearVesting.InsufficientBalance.selector); - vm.expectRevert(err); + uint256 amount = bound(amount_, 1, AMOUNT); // Call vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, AMOUNT, true); + linearVesting.redeem(derivativeTokenId, amount); + + // Check values + assertEq(linearVesting.balanceOf(_alice, derivativeTokenId), AMOUNT - amount); + assertEq(SoulboundCloneERC20(underlyingTokenAddress).balanceOf(_alice), amount); } - function test_redeem_wrapped(uint256 amount_) + function test_redeem_givenWrappedBalance(uint256 amount_) public givenWrappedDerivativeIsDeployed givenAliceHasWrappedDerivativeTokens(AMOUNT) @@ -1201,7 +1203,7 @@ contract LinearVestingTest is Test, Permit2User { // Call vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, amount, true); + linearVesting.redeem(derivativeTokenId, amount); // Check values assertEq(linearVesting.balanceOf(_alice, derivativeTokenId), 0); @@ -1209,7 +1211,7 @@ contract LinearVestingTest is Test, Permit2User { assertEq(SoulboundCloneERC20(underlyingTokenAddress).balanceOf(_alice), amount); } - function test_redeem_notWrapped(uint256 amount_) + function test_redeem_givenUnwrappedBalance(uint256 amount_) public givenWrappedDerivativeIsDeployed givenAliceHasDerivativeTokens(AMOUNT) @@ -1219,7 +1221,7 @@ contract LinearVestingTest is Test, Permit2User { // Call vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, amount, false); + linearVesting.redeem(derivativeTokenId, amount); // Check values assertEq(linearVesting.balanceOf(_alice, derivativeTokenId), AMOUNT - amount); @@ -1227,6 +1229,25 @@ contract LinearVestingTest is Test, Permit2User { assertEq(SoulboundCloneERC20(underlyingTokenAddress).balanceOf(_alice), amount); } + function test_redeem_givenMixedBalance() + public + givenWrappedDerivativeIsDeployed + givenAliceHasDerivativeTokens(AMOUNT) + givenAliceHasWrappedDerivativeTokens(AMOUNT) + givenAfterVestingExpiry + { + uint256 amountToRedeem = AMOUNT + 1; + + // Call + vm.prank(_alice); + linearVesting.redeem(derivativeTokenId, amountToRedeem); + + // Check values + assertEq(linearVesting.balanceOf(_alice, derivativeTokenId), 0); // Redeems unwrapped first + assertEq(SoulboundCloneERC20(derivativeWrappedAddress).balanceOf(_alice), AMOUNT - 1); + assertEq(SoulboundCloneERC20(underlyingTokenAddress).balanceOf(_alice), amountToRedeem); + } + // redeem max // [X] when the token id does not exist // [X] it reverts @@ -1246,7 +1267,7 @@ contract LinearVestingTest is Test, Permit2User { // Call vm.prank(_alice); - linearVesting.redeemMax(derivativeTokenId, false); + linearVesting.redeemMax(derivativeTokenId); } function test_redeemMax_givenRedeemableAmountIsZero_reverts() @@ -1259,56 +1280,49 @@ contract LinearVestingTest is Test, Permit2User { // Call vm.prank(_alice); - linearVesting.redeemMax(derivativeTokenId, false); + linearVesting.redeemMax(derivativeTokenId); } - function test_redeemMax_wrapped_givenWrappedTokenNotDeployed() + function test_redeemMax_givenWrappedTokenNotDeployed() public givenDerivativeIsDeployed givenAliceHasDerivativeTokens(AMOUNT) + givenAfterVestingExpiry { - // Expect revert - bytes memory err = abi.encodeWithSelector(LinearVesting.InsufficientBalance.selector); - vm.expectRevert(err); - // Call vm.prank(_alice); - linearVesting.redeemMax(derivativeTokenId, true); + linearVesting.redeemMax(derivativeTokenId); + + // Check values + assertEq(linearVesting.balanceOf(_alice, derivativeTokenId), 0); + assertEq(SoulboundCloneERC20(underlyingTokenAddress).balanceOf(_alice), AMOUNT); } - function test_redeemMax_wrapped(uint48 elapsed_) + function test_redeemMax_givenWrappedBalance_givenVestingExpiry() public givenWrappedDerivativeIsDeployed givenAliceHasWrappedDerivativeTokens(AMOUNT) + givenAfterVestingExpiry { - // Warp during vesting - uint48 elapsed = uint48(bound(elapsed_, 1, vestingDuration - 1)); - vm.warp(vestingStart + elapsed); - - uint256 redeemableAmount = elapsed * AMOUNT / vestingDuration; - // Call vm.prank(_alice); - linearVesting.redeemMax(derivativeTokenId, true); + linearVesting.redeemMax(derivativeTokenId); // Check values assertEq(linearVesting.balanceOf(_alice, derivativeTokenId), 0); - assertEq( - SoulboundCloneERC20(derivativeWrappedAddress).balanceOf(_alice), - AMOUNT - redeemableAmount - ); - assertEq(SoulboundCloneERC20(underlyingTokenAddress).balanceOf(_alice), redeemableAmount); + assertEq(SoulboundCloneERC20(derivativeWrappedAddress).balanceOf(_alice), 0); + assertEq(SoulboundCloneERC20(underlyingTokenAddress).balanceOf(_alice), AMOUNT); } - function test_redeemMax_wrapped_givenVestingExpiry() + function test_redeemMax_givenUnwrappedBalance_givenVestingExpiry() public givenWrappedDerivativeIsDeployed - givenAliceHasWrappedDerivativeTokens(AMOUNT) + givenAliceHasDerivativeTokens(AMOUNT) givenAfterVestingExpiry { // Call vm.prank(_alice); - linearVesting.redeemMax(derivativeTokenId, true); + linearVesting.redeemMax(derivativeTokenId); // Check values assertEq(linearVesting.balanceOf(_alice, derivativeTokenId), 0); @@ -1316,41 +1330,46 @@ contract LinearVestingTest is Test, Permit2User { assertEq(SoulboundCloneERC20(underlyingTokenAddress).balanceOf(_alice), AMOUNT); } - function test_redeemMax_notWrapped(uint48 elapsed_) - public - givenWrappedDerivativeIsDeployed - givenAliceHasDerivativeTokens(AMOUNT) - { + function test_redeemMax(uint48 elapsed_) public givenWrappedDerivativeIsDeployed { + // Mint both wrapped and unwrapped + _mintDerivativeTokens(_alice, AMOUNT); + _mintWrappedDerivativeTokens(_alice, AMOUNT); + // Warp during vesting uint48 elapsed = uint48(bound(elapsed_, 1, vestingDuration - 1)); vm.warp(vestingStart + elapsed); - uint256 redeemableAmount = elapsed * AMOUNT / vestingDuration; + uint256 redeemable = (AMOUNT + AMOUNT) * elapsed / vestingDuration; + uint256 expectedBalanceUnwrapped; + uint256 expectedBalanceWrapped; + if (redeemable < AMOUNT) { + expectedBalanceUnwrapped = AMOUNT - redeemable; + expectedBalanceWrapped = AMOUNT; + } else { + expectedBalanceUnwrapped = 0; + expectedBalanceWrapped = AMOUNT - (redeemable - AMOUNT); + } // Call vm.prank(_alice); - linearVesting.redeemMax(derivativeTokenId, false); + linearVesting.redeemMax(derivativeTokenId); // Check values - assertEq(linearVesting.balanceOf(_alice, derivativeTokenId), AMOUNT - redeemableAmount); - assertEq(SoulboundCloneERC20(derivativeWrappedAddress).balanceOf(_alice), 0); - assertEq(SoulboundCloneERC20(underlyingTokenAddress).balanceOf(_alice), redeemableAmount); - } - - function test_redeemMax_notWrapped_givenVestingExpiry() - public - givenWrappedDerivativeIsDeployed - givenAliceHasDerivativeTokens(AMOUNT) - givenAfterVestingExpiry - { - // Call - vm.prank(_alice); - linearVesting.redeemMax(derivativeTokenId, false); - - // Check values - assertEq(linearVesting.balanceOf(_alice, derivativeTokenId), 0); - assertEq(SoulboundCloneERC20(derivativeWrappedAddress).balanceOf(_alice), 0); - assertEq(SoulboundCloneERC20(underlyingTokenAddress).balanceOf(_alice), AMOUNT); + assertEq( + linearVesting.balanceOf(_alice, derivativeTokenId), + expectedBalanceUnwrapped, + "derivative token: balanceOf mismatch" + ); + assertEq( + SoulboundCloneERC20(derivativeWrappedAddress).balanceOf(_alice), + expectedBalanceWrapped, + "wrapped derivative token: balanceOf mismatch" + ); + assertEq( + SoulboundCloneERC20(underlyingTokenAddress).balanceOf(_alice), + redeemable, + "underlying token: balanceOf mismatch" + ); } // redeemable @@ -1385,69 +1404,48 @@ contract LinearVestingTest is Test, Permit2User { vm.expectRevert(err); // Call - linearVesting.redeemable(_alice, derivativeTokenId, false); + linearVesting.redeemable(_alice, derivativeTokenId); } - function test_redeemable_givenBlockTimestampIsBeforeStartTimestamp_returnsZero() + function test_redeemable_givenBeforeStart_returnsZero() public givenDerivativeIsDeployed givenAliceHasDerivativeTokens(AMOUNT) givenBeforeVestingStart { // Call - uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId, false); + uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId); // Check values assertEq(redeemableAmount, 0); } - function test_redeemable_givenBlockTimestampIsAfterExpiryTimestamp_returnsFullBalance() + function test_redeemable_givenAfterExpiry() public givenDerivativeIsDeployed givenAliceHasDerivativeTokens(AMOUNT) givenAfterVestingExpiry { // Call - uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId, false); + uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId); // Check values assertEq(redeemableAmount, AMOUNT); } - function test_redeemable_wrapped_givenWrappedTokenNotDeployed() + function test_redeemable_givenWrappedTokenNotDeployed() public givenDerivativeIsDeployed - givenAliceHasDerivativeTokens(AMOUNT) givenAfterVestingExpiry { // Call - uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId, true); + uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId); // Check values assertEq(redeemableAmount, 0); } - function test_redeemable_wrapped(uint256 amount_) - public - givenWrappedDerivativeIsDeployed - givenAliceHasDerivativeTokens(AMOUNT) - { - uint256 amount = bound(amount_, 1, AMOUNT); - - // Mint wrapped derivative tokens - _mintWrappedDerivativeTokens(_alice, amount); - - // Warp to expiry - vm.warp(vestingParams.expiry + 1); - - // Call - uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId, true); - - // Check values - assertEq(redeemableAmount, amount); // Does not include unwrapped derivative balance - } - - function test_redeemable_wrapped_givenBeforeExpiry(uint256 amount_) + function test_redeemable_givenBeforeExpiry(uint256 amount_) public givenWrappedDerivativeIsDeployed givenAliceHasDerivativeTokens(AMOUNT) @@ -1461,68 +1459,24 @@ contract LinearVestingTest is Test, Permit2User { uint48 elapsed = 100_000; vm.warp(vestingParams.start + elapsed); - uint256 expectedRedeemable = elapsed * amount / vestingDuration; - - // Call - uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId, true); - - // Check values - assertEq(redeemableAmount, expectedRedeemable); // Does not include unwrapped derivative balance - } - - function test_redeemable_notWrapped(uint256 amount_) - public - givenWrappedDerivativeIsDeployed - givenAliceHasWrappedDerivativeTokens(AMOUNT) - { - uint256 amount = bound(amount_, 1, AMOUNT); - - // Mint derivative tokens - _mintDerivativeTokens(_alice, amount); - - // Warp to expiry - vm.warp(vestingParams.expiry + 1); - - // Call - uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId, false); - - // Check values - assertEq(redeemableAmount, amount); // Does not include wwrapped derivative balance - } - - function test_redeemable_notWrapped_givenBeforeExpiry(uint256 amount_) - public - givenWrappedDerivativeIsDeployed - givenAliceHasWrappedDerivativeTokens(AMOUNT) - { - uint256 amount = bound(amount_, 1, AMOUNT); - - // Mint derivative tokens - _mintDerivativeTokens(_alice, amount); - - // Warp to before expiry - uint48 elapsed = 100_000; - vm.warp(vestingParams.start + elapsed); - - uint256 expectedRedeemable = elapsed * amount / vestingDuration; + // Includes wrapped and unwrapped balances + uint256 expectedRedeemable = elapsed * (AMOUNT + amount) / vestingDuration; // Call - uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId, false); + uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId); // Check values - assertEq(redeemableAmount, expectedRedeemable); // Does not include wrapped derivative balance + assertEq(redeemableAmount, expectedRedeemable); } - function test_redeemable_notWrapped_givenRedemption( + function test_redeemable_givenRedemption( uint256 wrappedAmount_, uint256 unwrappedAmount_, - uint256 wrappedRedeemPercentage_, - uint256 unwrappedRedeemPercentage_ + uint256 redeemPercentage_ ) public givenWrappedDerivativeIsDeployed { - uint256 wrappedAmount = bound(wrappedAmount_, 1, AMOUNT); - uint256 unwrappedAmount = bound(unwrappedAmount_, 1, AMOUNT); - uint256 wrappedRedeemPercentage = bound(wrappedRedeemPercentage_, 1, 100); - uint256 unwrappedRedeemPercentage = bound(unwrappedRedeemPercentage_, 1, 100); + uint256 wrappedAmount = bound(wrappedAmount_, 1e9, AMOUNT); + uint256 unwrappedAmount = bound(unwrappedAmount_, 1e9, AMOUNT); + uint256 redeemPercentage = bound(redeemPercentage_, 1, 100); // Mint wrapped derivative tokens _mintWrappedDerivativeTokens(_alice, wrappedAmount); @@ -1534,97 +1488,39 @@ contract LinearVestingTest is Test, Permit2User { uint48 elapsed = 50_000; vm.warp(vestingParams.start + elapsed); - // Redeem wrapped tokens - uint256 redeemableWrapped = elapsed * wrappedAmount / vestingDuration; - uint256 redeemAmountWrapped = redeemableWrapped * wrappedRedeemPercentage / 100; - if (redeemAmountWrapped > 0) { - vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, redeemAmountWrapped, true); - } - - // Redeem unwrapped tokens - uint256 redeemableUnwrapped = elapsed * unwrappedAmount / vestingDuration; - uint256 redeemAmountUnwrapped = redeemableUnwrapped * unwrappedRedeemPercentage / 100; - if (redeemAmountUnwrapped > 0) { - vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, redeemAmountUnwrapped, false); - } - - // Call - uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId, false); + // Calculate redeemable amount + uint256 redeemable = elapsed * (wrappedAmount + unwrappedAmount) / vestingDuration; + uint256 amountToRedeem = redeemable * redeemPercentage / 100; - // Check values - assertEq( - redeemableAmount, redeemableUnwrapped - redeemAmountUnwrapped, "redeemable mismatch" - ); // Not affected by the other balance - } - - function test_redeemable_wrapped_givenRedemption( - uint256 wrappedAmount_, - uint256 unwrappedAmount_, - uint256 wrappedRedeemPercentage_, - uint256 unwrappedRedeemPercentage_ - ) public givenWrappedDerivativeIsDeployed { - uint256 wrappedAmount = bound(wrappedAmount_, 1, AMOUNT); - uint256 unwrappedAmount = bound(unwrappedAmount_, 1, AMOUNT); - uint256 wrappedRedeemPercentage = bound(wrappedRedeemPercentage_, 1, 100); - uint256 unwrappedRedeemPercentage = bound(unwrappedRedeemPercentage_, 1, 100); - - // Mint wrapped derivative tokens - _mintWrappedDerivativeTokens(_alice, wrappedAmount); - - // Mint derivative tokens - _mintDerivativeTokens(_alice, unwrappedAmount); - - // Warp to before expiry - uint48 elapsed = 50_000; - vm.warp(vestingParams.start + elapsed); - - // Redeem wrapped tokens - uint256 redeemableWrapped = elapsed * wrappedAmount / vestingDuration; - uint256 redeemAmountWrapped = redeemableWrapped * wrappedRedeemPercentage / 100; - if (redeemAmountWrapped > 0) { - vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, redeemAmountWrapped, true); - } - - // Redeem unwrapped tokens - uint256 redeemableUnwrapped = elapsed * unwrappedAmount / vestingDuration; - uint256 redeemAmountUnwrapped = redeemableUnwrapped * unwrappedRedeemPercentage / 100; - if (redeemAmountUnwrapped > 0) { - vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, redeemAmountUnwrapped, false); - } + // Redeem + vm.prank(_alice); + linearVesting.redeem(derivativeTokenId, amountToRedeem); // Call - uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId, true); + uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId); // Check values - assertEq(redeemableAmount, redeemableWrapped - redeemAmountWrapped, "redeemable mismatch"); // Not affected by the other balance + assertEq(redeemableAmount, redeemable - amountToRedeem, "redeemable mismatch"); } - function test_redeemable_notWrapped_redemptions() public givenWrappedDerivativeIsDeployed { - // Mint derivative tokens + function test_redeemable_redemptions() public givenWrappedDerivativeIsDeployed { + // Mint tokens _mintDerivativeTokens(_alice, AMOUNT); + _mintWrappedDerivativeTokens(_alice, AMOUNT); // Warp to before expiry uint48 elapsed = 50_000; vm.warp(vestingParams.start + elapsed); // Calculate the vested amount - uint256 vestedAmount = elapsed * AMOUNT / vestingDuration; + uint256 vestedAmount = elapsed * (AMOUNT + AMOUNT) / vestingDuration; uint256 claimedAmount = 0; uint256 redeemableAmount = vestedAmount - claimedAmount; assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, false), + linearVesting.redeemable(_alice, derivativeTokenId), redeemableAmount, - "1: redeemable mismatch, unwrapped" - ); - assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, true), - 0, - "1: redeemable mismatch, wrapped" + "1: redeemable mismatch" ); // Redeem half the tokens @@ -1633,17 +1529,12 @@ contract LinearVestingTest is Test, Permit2User { redeemableAmount = vestedAmount - claimedAmount; vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, redeemAmount, false); + linearVesting.redeem(derivativeTokenId, redeemAmount); assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, false), + linearVesting.redeemable(_alice, derivativeTokenId), redeemableAmount, - "2: redeemable mismatch, unwrapped" - ); - assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, true), - 0, - "2: redeemable mismatch, wrapped" + "2: redeemable mismatch" ); // Redeem the remaining tokens @@ -1652,17 +1543,12 @@ contract LinearVestingTest is Test, Permit2User { redeemableAmount = 0; vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, redeemAmount, false); + linearVesting.redeem(derivativeTokenId, redeemAmount); assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, false), + linearVesting.redeemable(_alice, derivativeTokenId), redeemableAmount, - "3: redeemable mismatch, unwrapped" - ); - assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, true), - 0, - "3: redeemable mismatch, wrapped" + "3: redeemable mismatch" ); // Check that the claimed amount is the same as the vested amount @@ -1673,18 +1559,13 @@ contract LinearVestingTest is Test, Permit2User { vm.warp(vestingParams.start + elapsed); // Calculate the vested amount - vestedAmount = elapsed * AMOUNT / vestingDuration; + vestedAmount = elapsed * (AMOUNT + AMOUNT) / vestingDuration; redeemableAmount = vestedAmount - claimedAmount; assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, false), + linearVesting.redeemable(_alice, derivativeTokenId), redeemableAmount, - "4: redeemable mismatch, unwrapped" - ); - assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, true), - 0, - "4: redeemable mismatch, wrapped" + "4: redeemable mismatch" ); // Redeem half the tokens @@ -1693,18 +1574,13 @@ contract LinearVestingTest is Test, Permit2User { redeemableAmount = vestedAmount - claimedAmount; vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, redeemAmount, false); + linearVesting.redeem(derivativeTokenId, redeemAmount); assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, false), + linearVesting.redeemable(_alice, derivativeTokenId), redeemableAmount, "5: redeemable mismatch" ); - assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, true), - 0, - "5: redeemable mismatch, wrapped" - ); // Redeem the remaining tokens redeemAmount = redeemableAmount; @@ -1712,191 +1588,72 @@ contract LinearVestingTest is Test, Permit2User { redeemableAmount = 0; vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, redeemAmount, false); + linearVesting.redeem(derivativeTokenId, redeemAmount); assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, false), + linearVesting.redeemable(_alice, derivativeTokenId), redeemableAmount, "6: redeemable mismatch" ); - assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, true), - 0, - "6: redeemable mismatch, wrapped" - ); // Check that the claimed amount is the same as the vested amount assertEq(claimedAmount, vestedAmount, "claimedAmount mismatch"); } - function test_redeemable_wrapped_redemptions() public givenWrappedDerivativeIsDeployed { - // Mint derivative tokens - _mintWrappedDerivativeTokens(_alice, AMOUNT); - - // Warp to before expiry - uint48 elapsed = 50_000; - vm.warp(vestingParams.start + elapsed); - - // Calculate the vested amount - uint256 vestedAmount = elapsed * AMOUNT / vestingDuration; - uint256 claimedAmount = 0; - uint256 redeemableAmount = vestedAmount - claimedAmount; - - assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, false), - 0, - "1: redeemable mismatch, unwrapped" - ); - assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, true), - redeemableAmount, - "1: redeemable mismatch, wrapped" - ); - - // Redeem half the tokens - uint256 redeemAmount = redeemableAmount / 2; - claimedAmount += redeemAmount; - redeemableAmount = vestedAmount - claimedAmount; - - vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, redeemAmount, true); - - assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, false), - 0, - "2: redeemable mismatch, unwrapped" - ); - assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, true), - redeemableAmount, - "2: redeemable mismatch, wrapped" - ); - - // Redeem the remaining tokens - redeemAmount = redeemableAmount; - claimedAmount += redeemAmount; - redeemableAmount = 0; - - vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, redeemAmount, true); - - assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, false), - 0, - "3: redeemable mismatch, unwrapped" - ); - assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, true), - redeemableAmount, - "3: redeemable mismatch, wrapped" - ); - - // Warp to another time - elapsed = 60_000; - vm.warp(vestingParams.start + elapsed); - - // Calculate the vested amount - vestedAmount = elapsed * AMOUNT / vestingDuration; - redeemableAmount = vestedAmount - claimedAmount; - - assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, false), - 0, - "4: redeemable mismatch, unwrapped" - ); - assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, true), - redeemableAmount, - "4: redeemable mismatch, wrapped" - ); - - // Redeem half the tokens - redeemAmount = redeemableAmount / 2; - claimedAmount += redeemAmount; - redeemableAmount = vestedAmount - claimedAmount; - - vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, redeemAmount, true); - - assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, false), 0, "5: redeemable mismatch" - ); - assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, true), - redeemableAmount, - "5: redeemable mismatch, wrapped" - ); - - // Redeem the remaining tokens - redeemAmount = redeemableAmount; - claimedAmount += redeemAmount; - redeemableAmount = 0; - - vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, redeemAmount, true); - - assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, false), 0, "6: redeemable mismatch" - ); - assertEq( - linearVesting.redeemable(_alice, derivativeTokenId, true), - redeemableAmount, - "6: redeemable mismatch, wrapped" - ); - } - - function test_redeemable_notWrapped_givenTokensMintedAfterDeployment() + function test_redeemable_givenTokensMintedAfterDeployment(uint256 amount_) public givenWrappedDerivativeIsDeployed givenAliceHasWrappedDerivativeTokens(AMOUNT) { + uint256 amount = bound(amount_, 1e9, AMOUNT); + // Warp to before expiry uint48 elapsed = 50_000; vm.warp(vestingParams.start + elapsed); // Mint tokens - _mintDerivativeTokens(_alice, AMOUNT); + _mintDerivativeTokens(_alice, amount); - uint256 expectedRedeemable = elapsed * AMOUNT / vestingDuration; + uint256 expectedRedeemable = elapsed * (AMOUNT + amount) / vestingDuration; // Call - uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId, false); + uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId); // Check values - assertEq(redeemableAmount, expectedRedeemable); // Does not include wrapped derivative balance + assertEq(redeemableAmount, expectedRedeemable); } - function test_redeemable_wrapped_givenTokensMintedAfterDeployment() + function test_redeemable_givenWrappedTokensMintedAfterDeployment(uint256 amount_) public givenWrappedDerivativeIsDeployed givenAliceHasDerivativeTokens(AMOUNT) { + uint256 amount = bound(amount_, 1e9, AMOUNT); + // Warp to before expiry uint48 elapsed = 50_000; vm.warp(vestingParams.start + elapsed); // Mint tokens - _mintWrappedDerivativeTokens(_alice, AMOUNT); + _mintWrappedDerivativeTokens(_alice, amount); - uint256 expectedRedeemable = elapsed * AMOUNT / vestingDuration; + uint256 expectedRedeemable = elapsed * (AMOUNT + amount) / vestingDuration; // Call - uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId, true); + uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId); // Check values - assertEq(redeemableAmount, expectedRedeemable); // Does not include unwrapped derivative balance + assertEq(redeemableAmount, expectedRedeemable); } - function test_redeemable_notWrapped_givenRedemption_givenTokensMintedAfterDeployment( + function test_redeemable_givenRedemption_givenTokensMintedAfterDeployment( uint256 wrappedAmount_, uint256 unwrappedAmount_, - uint256 wrappedRedeemPercentage_, - uint256 unwrappedRedeemPercentage_ + uint256 redeemPercentage_ ) public givenWrappedDerivativeIsDeployed { - uint256 wrappedAmount = bound(wrappedAmount_, 1, AMOUNT); - uint256 unwrappedAmount = bound(unwrappedAmount_, 1, AMOUNT); - uint256 wrappedRedeemPercentage = bound(wrappedRedeemPercentage_, 1, 100); - uint256 unwrappedRedeemPercentage = bound(unwrappedRedeemPercentage_, 1, 100); + uint256 wrappedAmount = bound(wrappedAmount_, 1e9, AMOUNT); + uint256 unwrappedAmount = bound(unwrappedAmount_, 1e9, AMOUNT); + uint256 redeemPercentage = bound(redeemPercentage_, 1, 100); // Mint wrapped derivative tokens _mintWrappedDerivativeTokens(_alice, wrappedAmount); @@ -1909,95 +1666,34 @@ contract LinearVestingTest is Test, Permit2User { vm.warp(vestingParams.start + elapsed); // Redeem wrapped tokens - uint256 redeemableWrapped = elapsed * wrappedAmount / vestingDuration; - uint256 redeemAmountWrapped = redeemableWrapped * wrappedRedeemPercentage / 100; - if (redeemAmountWrapped > 0) { - vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, redeemAmountWrapped, true); - } + uint256 redeemable = elapsed * (wrappedAmount + unwrappedAmount) / vestingDuration; + uint256 amountToRedeem = redeemable * redeemPercentage / 100; - // Redeem unwrapped tokens - uint256 redeemableUnwrapped = elapsed * unwrappedAmount / vestingDuration; - uint256 redeemAmountUnwrapped = redeemableUnwrapped * unwrappedRedeemPercentage / 100; - if (redeemAmountUnwrapped > 0) { - vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, redeemAmountUnwrapped, false); - } + vm.prank(_alice); + linearVesting.redeem(derivativeTokenId, amountToRedeem); // Mint more tokens _mintDerivativeTokens(_alice, AMOUNT); - - // Warp to another time - elapsed = 60_000; - vm.warp(vestingParams.start + elapsed); - - uint256 totalAmountUnwrapped = unwrappedAmount + AMOUNT; - uint256 expectedRedeemableUnwrapped = - elapsed * totalAmountUnwrapped / vestingDuration - redeemAmountUnwrapped; - - // Call - uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId, false); - - // Check values - assertEq(redeemableAmount, expectedRedeemableUnwrapped, "redeemable mismatch"); // Not affected by the other balance - } - - function test_redeemable_wrapped_givenRedemption_givenTokensMintedAfterDeployment( - uint256 wrappedAmount_, - uint256 unwrappedAmount_, - uint256 wrappedRedeemPercentage_, - uint256 unwrappedRedeemPercentage_ - ) public givenWrappedDerivativeIsDeployed { - uint256 wrappedAmount = bound(wrappedAmount_, 1, AMOUNT); - uint256 unwrappedAmount = bound(unwrappedAmount_, 1, AMOUNT); - uint256 wrappedRedeemPercentage = bound(wrappedRedeemPercentage_, 1, 100); - uint256 unwrappedRedeemPercentage = bound(unwrappedRedeemPercentage_, 1, 100); - - // Mint wrapped derivative tokens - _mintWrappedDerivativeTokens(_alice, wrappedAmount); - - // Mint derivative tokens - _mintDerivativeTokens(_alice, unwrappedAmount); - - // Warp to before expiry - uint48 elapsed = 50_000; - vm.warp(vestingParams.start + elapsed); - - // Redeem wrapped tokens - uint256 redeemableWrapped = elapsed * wrappedAmount / vestingDuration; - uint256 redeemAmountWrapped = redeemableWrapped * wrappedRedeemPercentage / 100; - if (redeemAmountWrapped > 0) { - vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, redeemAmountWrapped, true); - } - - // Redeem unwrapped tokens - uint256 redeemableUnwrapped = elapsed * unwrappedAmount / vestingDuration; - uint256 redeemAmountUnwrapped = redeemableUnwrapped * unwrappedRedeemPercentage / 100; - if (redeemAmountUnwrapped > 0) { - vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, redeemAmountUnwrapped, false); - } - - // Mint wrapped tokens _mintWrappedDerivativeTokens(_alice, AMOUNT); // Warp to another time elapsed = 60_000; vm.warp(vestingParams.start + elapsed); - uint256 totalAmountWrapped = wrappedAmount + AMOUNT; - uint256 expectedRedeemableWrapped = - elapsed * totalAmountWrapped / vestingDuration - redeemAmountWrapped; + // Calculate the vested amount + uint256 vestedAmount = + elapsed * (AMOUNT + AMOUNT + unwrappedAmount + wrappedAmount) / vestingDuration; + uint256 claimedAmount = amountToRedeem; + uint256 expectedRedeemableAmount = vestedAmount - claimedAmount; // Call - uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId, true); + uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId); // Check values - assertEq(redeemableAmount, expectedRedeemableWrapped, "redeemable mismatch"); // Not affected by the other balance + assertEq(redeemableAmount, expectedRedeemableAmount, "redeemable mismatch"); } - function test_redeemable_wrapped_givenWrappedRedeemed_givenUnwrapped() + function test_redeemable_givenRedemption_givenUnwrapped() public givenWrappedDerivativeIsDeployed givenAliceHasDerivativeTokens(AMOUNT) @@ -2007,48 +1703,38 @@ contract LinearVestingTest is Test, Permit2User { uint48 elapsed = 50_000; vm.warp(vestingParams.start + elapsed); - uint256 vestedUnwrapped = elapsed * AMOUNT / vestingDuration; - uint256 vestedWrapped = elapsed * AMOUNT / vestingDuration; + uint256 vested = (AMOUNT + AMOUNT).mulDivDown(elapsed, vestingDuration); - // Redeem wrapped tokens - partial amount - uint256 redeemAmountWrapped = vestedWrapped / 2; + // Redeem tokens - partial amount + uint256 redeemAmount = 1e9; vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, redeemAmountWrapped, true); + linearVesting.redeem(derivativeTokenId, redeemAmount); - // Unwrap the remaining wrapped tokens - uint256 wrappedToUnwrap = AMOUNT - redeemAmountWrapped; + // Unwrap half of the remaining wrapped tokens + uint256 wrappedToUnwrap = + SoulboundCloneERC20(derivativeWrappedAddress).balanceOf(_alice) / 2; vm.prank(_alice); linearVesting.unwrap(derivativeTokenId, wrappedToUnwrap); - // Check the unwrapped redeemable amount - uint256 redeemableUnwrapped = linearVesting.redeemable(_alice, derivativeTokenId, false); - assertEq( - redeemableUnwrapped, vestedUnwrapped + wrappedToUnwrap, "unwrapped: redeemable mismatch" - ); + uint256 expectedRedeemableAmount = vested - redeemAmount; - // Check the wrapped redeemable amount - uint256 redeemableWrapped = linearVesting.redeemable(_alice, derivativeTokenId, true); - assertEq(redeemableWrapped, 0, "wrapped: redeemable mismatch"); + // Check the redeemable amount + uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId); + assertEq(redeemableAmount, expectedRedeemableAmount, "redeemable mismatch"); // Warp to another time elapsed = 60_000; vm.warp(vestingParams.start + elapsed); - vestedUnwrapped = elapsed * AMOUNT / vestingDuration; - vestedWrapped = elapsed * wrappedToUnwrap / vestingDuration; - - // Check the unwrapped redeemable amount - redeemableUnwrapped = linearVesting.redeemable(_alice, derivativeTokenId, false); - assertEq( - redeemableUnwrapped, vestedUnwrapped + vestedWrapped, "unwrapped: redeemable mismatch" - ); + vested = (AMOUNT + AMOUNT).mulDivDown(elapsed, vestingDuration); + expectedRedeemableAmount = vested - redeemAmount; - // Check the wrapped redeemable amount - redeemableWrapped = linearVesting.redeemable(_alice, derivativeTokenId, true); - assertEq(redeemableWrapped, 0, "wrapped: redeemable mismatch"); + // Check the redeemable amount + redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId); + assertEq(redeemableAmount, expectedRedeemableAmount, "redeemable mismatch"); } - function test_redeemable_unwrapped_givenUnwrappedRedeemed_givenWrapped() + function test_redeemable_givenRedemption_givenWrapped() public givenWrappedDerivativeIsDeployed givenAliceHasDerivativeTokens(AMOUNT) @@ -2058,104 +1744,34 @@ contract LinearVestingTest is Test, Permit2User { uint48 elapsed = 50_000; vm.warp(vestingParams.start + elapsed); - uint256 vestedUnwrapped = elapsed * AMOUNT / vestingDuration; - uint256 vestedWrapped = elapsed * AMOUNT / vestingDuration; + uint256 vested = (AMOUNT + AMOUNT).mulDivDown(elapsed, vestingDuration); - // Redeem unwrapped tokens - partial amount - uint256 redeemAmountUnwrapped = vestedUnwrapped / 2; + // Redeem tokens - partial amount + uint256 redeemAmount = vested / 4; vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, redeemAmountUnwrapped, false); + linearVesting.redeem(derivativeTokenId, redeemAmount); - // Wrap the remaining unwrapped tokens - uint256 unwrappedToWrap = AMOUNT - redeemAmountUnwrapped; + // Wrap half of the remaining unwrapped tokens + uint256 unwrappedToWrap = linearVesting.balanceOf(_alice, derivativeTokenId) / 2; vm.prank(_alice); linearVesting.wrap(derivativeTokenId, unwrappedToWrap); - // Check the unwrapped redeemable amount - uint256 redeemableUnwrapped = linearVesting.redeemable(_alice, derivativeTokenId, false); - assertEq(redeemableUnwrapped, 0, "unwrapped: redeemable mismatch after wrap"); + uint256 expectedRedeemableAmount = vested - redeemAmount; - // Check the wrapped redeemable amount - uint256 redeemableWrapped = linearVesting.redeemable(_alice, derivativeTokenId, true); - assertEq( - redeemableWrapped, - vestedWrapped + unwrappedToWrap, - "wrapped: redeemable mismatch after wrap" - ); + // Check the redeemable amount + uint256 redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId); + assertEq(redeemableAmount, expectedRedeemableAmount, "redeemable mismatch"); // Warp to another time elapsed = 60_000; vm.warp(vestingParams.start + elapsed); - vestedUnwrapped = elapsed * unwrappedToWrap / vestingDuration; - vestedWrapped = elapsed * AMOUNT / vestingDuration; - - // Check the unwrapped redeemable amount - redeemableUnwrapped = linearVesting.redeemable(_alice, derivativeTokenId, false); - assertEq(redeemableUnwrapped, 0, "unwrapped: redeemable mismatch"); - - // Check the wrapped redeemable amount - redeemableWrapped = linearVesting.redeemable(_alice, derivativeTokenId, true); - assertEq(redeemableWrapped, vestedUnwrapped + vestedWrapped, "wrapped: redeemable mismatch"); - } - - function test_redeemable_unwrapped_givenUnwrappedRedeemed_givenPartialWrapped() - public - givenWrappedDerivativeIsDeployed - givenAliceHasDerivativeTokens(AMOUNT) - givenAliceHasWrappedDerivativeTokens(AMOUNT) - { - // Warp to before expiry - uint48 elapsed = 50_000; - vm.warp(vestingParams.start + elapsed); - - uint256 vestedUnwrapped = elapsed * AMOUNT / vestingDuration; + vested = (AMOUNT + AMOUNT).mulDivDown(elapsed, vestingDuration); + expectedRedeemableAmount = vested - redeemAmount; - // Redeem unwrapped tokens - partial amount - uint256 redeemAmountUnwrapped = vestedUnwrapped / 2; - vm.prank(_alice); - linearVesting.redeem(derivativeTokenId, redeemAmountUnwrapped, false); - - // Wrap some of the remaining unwrapped tokens - uint256 unwrappedToWrap = (AMOUNT - redeemAmountUnwrapped) / 2; - vm.prank(_alice); - linearVesting.wrap(derivativeTokenId, unwrappedToWrap); - - // Check the unwrapped redeemable amount - uint256 redeemableUnwrapped = linearVesting.redeemable(_alice, derivativeTokenId, false); - assertEq( - redeemableUnwrapped, - elapsed * (AMOUNT - unwrappedToWrap) / vestingDuration - redeemAmountUnwrapped, - "unwrapped: redeemable mismatch after wrap" - ); - - // Check the wrapped redeemable amount - uint256 redeemableWrapped = linearVesting.redeemable(_alice, derivativeTokenId, true); - assertEq( - redeemableWrapped, - elapsed * (AMOUNT + unwrappedToWrap) / vestingDuration, - "wrapped: redeemable mismatch after wrap" - ); - - // Warp to another time - elapsed = 60_000; - vm.warp(vestingParams.start + elapsed); - - // Check the unwrapped redeemable amount - redeemableUnwrapped = linearVesting.redeemable(_alice, derivativeTokenId, false); - assertEq( - redeemableUnwrapped, - elapsed * (AMOUNT - unwrappedToWrap) / vestingDuration - redeemAmountUnwrapped, - "unwrapped: redeemable mismatch" - ); - - // Check the wrapped redeemable amount - redeemableWrapped = linearVesting.redeemable(_alice, derivativeTokenId, true); - assertEq( - redeemableWrapped, - elapsed * (AMOUNT + unwrappedToWrap) / vestingDuration, - "wrapped: redeemable mismatch" - ); + // Check the redeemable amount + redeemableAmount = linearVesting.redeemable(_alice, derivativeTokenId); + assertEq(redeemableAmount, expectedRedeemableAmount, "redeemable mismatch"); } // wrap @@ -2607,7 +2223,7 @@ contract LinearVestingTest is Test, Permit2User { vm.expectRevert(err); // Call - linearVesting.transform(derivativeTokenId, _alice, AMOUNT, false); + linearVesting.transform(derivativeTokenId, _alice, AMOUNT); } // exercise @@ -2619,6 +2235,6 @@ contract LinearVestingTest is Test, Permit2User { vm.expectRevert(err); // Call - linearVesting.exercise(derivativeTokenId, AMOUNT, false); + linearVesting.exercise(derivativeTokenId, AMOUNT); } } diff --git a/test/modules/derivatives/LinearVestingIntegration.t.sol b/test/modules/derivatives/LinearVestingIntegration.t.sol index 6bcd7d0a..c7f5adb1 100644 --- a/test/modules/derivatives/LinearVestingIntegration.t.sol +++ b/test/modules/derivatives/LinearVestingIntegration.t.sol @@ -383,9 +383,7 @@ contract LinearVestingIntegrationTest is Test, Permit2User { ); // Derivative token is not yet redeemable - assertEq( - linearVesting.redeemable(_recipient, derivativeTokenId, false), 0, "redeemable mismatch" - ); + assertEq(linearVesting.redeemable(_recipient, derivativeTokenId), 0, "redeemable mismatch"); // Derivative token cannot be transferred bytes memory err = abi.encodeWithSelector(LinearVesting.NotPermitted.selector); @@ -487,9 +485,7 @@ contract LinearVestingIntegrationTest is Test, Permit2User { ); // Derivative token is not yet redeemable - assertEq( - linearVesting.redeemable(_recipient, derivativeTokenId, false), 0, "redeemable mismatch" - ); + assertEq(linearVesting.redeemable(_recipient, derivativeTokenId), 0, "redeemable mismatch"); // Derivative token cannot be transferred bytes memory err = abi.encodeWithSelector(LinearVesting.NotPermitted.selector); diff --git a/test/modules/derivatives/mocks/MockDerivativeModule.sol b/test/modules/derivatives/mocks/MockDerivativeModule.sol index b87e8490..fe7b912c 100644 --- a/test/modules/derivatives/mocks/MockDerivativeModule.sol +++ b/test/modules/derivatives/mocks/MockDerivativeModule.sol @@ -106,17 +106,16 @@ contract MockDerivativeModule is DerivativeModule { bool wrapped_ ) external virtual override returns (uint256, address, uint256) {} - function redeem(uint256 tokenId_, uint256 amount_, bool wrapped_) external virtual override {} + function redeem(uint256 tokenId_, uint256 amount_) external virtual override {} - function exercise(uint256 tokenId_, uint256 amount, bool wrapped_) external virtual override {} + function exercise(uint256 tokenId_, uint256 amount) external virtual override {} function reclaim(uint256 tokenId_) external virtual override {} function transform( uint256 tokenId_, address from_, - uint256 amount_, - bool wrapped_ + uint256 amount_ ) external virtual override {} function wrap(uint256 tokenId_, uint256 amount_) external virtual override {} @@ -203,12 +202,11 @@ contract MockDerivativeModule is DerivativeModule { return (tokenId, token.wrapped); } - function redeemMax(uint256 tokenId_, bool wrapped_) external virtual override {} + function redeemMax(uint256 tokenId_) external virtual override {} function redeemable( address owner_, - uint256 tokenId_, - bool wrapped_ + uint256 tokenId_ ) external view virtual override returns (uint256) {} function name(uint256 tokenId_) public view virtual override returns (string memory) {}