From 193fa602c0cd0d351a71b319d7913fdca9844f63 Mon Sep 17 00:00:00 2001 From: Sandy Bradley Date: Sun, 25 Aug 2024 10:34:20 +0200 Subject: [PATCH 1/3] feat: deploy (#14) * feat: deploy * fix: import semantics --- script/FoldCaptiveStaking.s.sol | 26 ++++++++++++++++++++++++++ script/deploy-fork.sh | 10 ++++++++++ script/deploy.sh | 12 ++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 script/FoldCaptiveStaking.s.sol create mode 100644 script/deploy-fork.sh create mode 100644 script/deploy.sh diff --git a/script/FoldCaptiveStaking.s.sol b/script/FoldCaptiveStaking.s.sol new file mode 100644 index 0000000..4850323 --- /dev/null +++ b/script/FoldCaptiveStaking.s.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "forge-std/Script.sol"; +import "src/FoldCaptiveStaking.sol"; + +contract FoldCaptiveStakingScript is Script { + INonfungiblePositionManager public positionManager = + INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88); + IUniswapV3Pool public pool = IUniswapV3Pool(0x5eCEf3b72Cb00DBD8396EBAEC66E0f87E9596e97); + WETH public weth = WETH(payable(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2)); + ERC20 public fold = ERC20(0xd084944d3c05CD115C09d072B9F44bA3E0E45921); + + + function run() public { + vm.startBroadcast(); + FoldCaptiveStaking foldCaptiveStaking = + new FoldCaptiveStaking(address(positionManager), address(pool), address(weth), address(fold)); + + fold.transfer(address(foldCaptiveStaking), 1_000_000); + weth.transfer(address(foldCaptiveStaking), 1_000_000); + + foldCaptiveStaking.initialize(); + vm.stopBroadcast(); + } +} \ No newline at end of file diff --git a/script/deploy-fork.sh b/script/deploy-fork.sh new file mode 100644 index 0000000..7e23098 --- /dev/null +++ b/script/deploy-fork.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +source .env + +forge script script/FoldCaptiveStaking.s.sol:FoldCaptiveStakingScript \ + --chain-id 1 \ + --fork-url $RPC_MAINNET \ + --broadcast \ + --private-key $PRIVATE_KEY \ + -vvv \ No newline at end of file diff --git a/script/deploy.sh b/script/deploy.sh new file mode 100644 index 0000000..9217e69 --- /dev/null +++ b/script/deploy.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +source .env + +forge script script/FoldCaptiveStaking.s.sol:FoldCaptiveStakingScript \ + --chain-id 1 \ + --rpc-url $RPC_MAINNET \ + --broadcast \ + --private-key $PRIVATE_KEY \ + --verify \ + --etherscan-api-key $ETHERSCAN_API_KEY \ + -vvv \ No newline at end of file From e60e93e6cdea225fbe4e26ecae1954f016193178 Mon Sep 17 00:00:00 2001 From: Sandy Bradley Date: Sun, 25 Aug 2024 10:46:19 +0200 Subject: [PATCH 2/3] feat: 14 day cooldown withdraw, deposit min (#13) --- src/FoldCaptiveStaking.sol | 24 +++++++---- test/BaseCaptiveTest.sol | 2 + test/UnitTests.t.sol | 85 ++++++++++++++------------------------ 3 files changed, 47 insertions(+), 64 deletions(-) diff --git a/src/FoldCaptiveStaking.sol b/src/FoldCaptiveStaking.sol index 6af2cfb..855e702 100644 --- a/src/FoldCaptiveStaking.sol +++ b/src/FoldCaptiveStaking.sol @@ -40,8 +40,9 @@ contract FoldCaptiveStaking is Owned(msg.sender) { error NotInitialized(); error ZeroLiquidity(); error WithdrawFailed(); - error WithdrawProRata(); error DepositCapReached(); + error DepositAmountBelowMinimum(); + error WithdrawalCooldownPeriodNotMet(); /// @param _positionManager The Canonical UniswapV3 PositionManager /// @param _pool The FOLD Pool to Reward @@ -138,6 +139,13 @@ contract FoldCaptiveStaking is Owned(msg.sender) { /// @dev The cap on deposits in the pool in liquidity, set to 0 if no cap uint256 public depositCap; + /// @dev Min deposit amount for Fold / Eth + uint256 public constant MINIMUM_DEPOSIT = 1 ether; + /// @dev Min lockup period + uint256 public constant COOLDOWN_PERIOD = 14 days; + + mapping(address => uint256) public depositTimeStamp; + /*////////////////////////////////////////////////////////////// CHEF //////////////////////////////////////////////////////////////*/ @@ -175,6 +183,8 @@ contract FoldCaptiveStaking is Owned(msg.sender) { /// @param amount1 The amount of token1 to deposit /// @param slippage Slippage on deposit out of 1e18 function deposit(uint256 amount0, uint256 amount1, uint256 slippage) external isInitialized { + if (amount0 < MINIMUM_DEPOSIT && amount1 < MINIMUM_DEPOSIT) revert DepositAmountBelowMinimum(); + collectFees(); collectRewards(); @@ -207,6 +217,8 @@ contract FoldCaptiveStaking is Owned(msg.sender) { revert DepositCapReached(); } + depositTimeStamp[msg.sender] = block.timestamp; + emit Deposit(msg.sender, amount0, amount1); } @@ -276,13 +288,11 @@ contract FoldCaptiveStaking is Owned(msg.sender) { /// @notice Withdraws liquidity from the pool /// @param liquidity The amount of liquidity to withdraw function withdraw(uint128 liquidity) external isInitialized { + if (block.timestamp < depositTimeStamp[msg.sender] + COOLDOWN_PERIOD) revert WithdrawalCooldownPeriodNotMet(); + collectFees(); collectRewards(); - if (liquidity > balances[msg.sender].amount / 2) { - revert WithdrawProRata(); - } - balances[msg.sender].amount -= liquidity; liquidityUnderManagement -= uint256(liquidity); @@ -349,10 +359,6 @@ contract FoldCaptiveStaking is Owned(msg.sender) { collectPositionFees(); collectRewards(); - if (liquidity > liquidityUnderManagement / 2) { - revert WithdrawProRata(); - } - liquidityUnderManagement -= uint256(liquidity); INonfungiblePositionManager.DecreaseLiquidityParams memory decreaseParams = INonfungiblePositionManager diff --git a/test/BaseCaptiveTest.sol b/test/BaseCaptiveTest.sol index 1478c46..2892a66 100644 --- a/test/BaseCaptiveTest.sol +++ b/test/BaseCaptiveTest.sol @@ -11,6 +11,8 @@ contract BaseCaptiveTest is Test { error NotInitialized(); error ZeroLiquidity(); error WithdrawFailed(); + error DepositAmountBelowMinimum(); + error WithdrawalCooldownPeriodNotMet(); error WithdrawProRata(); error DepositCapReached(); diff --git a/test/UnitTests.t.sol b/test/UnitTests.t.sol index 3d8c969..deeddb7 100644 --- a/test/UnitTests.t.sol +++ b/test/UnitTests.t.sol @@ -31,6 +31,9 @@ contract UnitTests is BaseCaptiveTest { function testRemoveLiquidity() public { testAddLiquidity(); + // Simulate passage of cooldown period + vm.warp(block.timestamp + 14 days); + (uint128 amount, uint128 rewardDebt, uint128 token0FeeDebt, uint128 token1FeeDebt) = foldCaptiveStaking.balances(User01); @@ -218,6 +221,9 @@ contract UnitTests is BaseCaptiveTest { assertEq(rewardDebt, foldCaptiveStaking.rewardsPerLiquidity()); assertGt(weth.balanceOf(User01), initialBalance); + // Simulate passage of cooldown period + vm.warp(block.timestamp + 14 days); + (uint128 liq,,,) = foldCaptiveStaking.balances(User01); foldCaptiveStaking.withdraw(liq / 3); } @@ -242,23 +248,6 @@ contract UnitTests is BaseCaptiveTest { vm.stopPrank(); } - /// @dev Ensure pro-rata withdrawals are handled correctly - function testProRataWithdrawals() public { - testAddLiquidity(); - - (uint128 liq,,,) = foldCaptiveStaking.balances(User01); - - // Attempt to withdraw more than allowed amount - vm.expectRevert(WithdrawProRata.selector); - foldCaptiveStaking.withdraw(liq); - - // Pro-rated withdrawal - foldCaptiveStaking.withdraw(liq / 2); - (uint128 amount,,,) = foldCaptiveStaking.balances(User01); - assertEq(amount, liq / 2); - } - - /// @dev Ensure zero deposits are handled correctly and revert as expected. function testZeroDeposit() public { vm.expectRevert(); foldCaptiveStaking.deposit(0, 0, 0); @@ -279,6 +268,21 @@ contract UnitTests is BaseCaptiveTest { attack.attack(); } + function testMinimumDeposit() public { + fold.transfer(User01, 0.5 ether); + + vm.deal(User01, 0.5 ether); + vm.startPrank(User01); + + weth.deposit{value: 0.5 ether}(); + weth.approve(address(foldCaptiveStaking), type(uint256).max); + fold.approve(address(foldCaptiveStaking), type(uint256).max); + + // Expect revert due to minimum deposit requirement + vm.expectRevert(DepositAmountBelowMinimum.selector); + foldCaptiveStaking.deposit(0.5 ether, 0.5 ether, 0); + } + /// @dev Deposit Cap Enforcement: Test to ensure the deposit cap is respected. function testDepositCap() public { uint256 cap = 100 ether; @@ -303,52 +307,23 @@ contract UnitTests is BaseCaptiveTest { vm.stopPrank(); } - /// @dev Multiple Users: Test simultaneous deposits and withdrawals by multiple users. - function testMultipleUsersDepositWithdraw() public { - // User 1 deposits - fold.transfer(User01, 1_000 ether); - vm.deal(User01, 1_000 ether); - vm.startPrank(User01); - - weth.deposit{value: 1_000 ether}(); - weth.approve(address(foldCaptiveStaking), type(uint256).max); - fold.approve(address(foldCaptiveStaking), type(uint256).max); - - foldCaptiveStaking.deposit(1_000 ether, 1_000 ether, 0); - - vm.stopPrank(); - - // User 2 deposits - fold.transfer(User02, 500 ether); - vm.deal(User02, 500 ether); - vm.startPrank(User02); - - weth.deposit{value: 500 ether}(); - weth.approve(address(foldCaptiveStaking), type(uint256).max); - fold.approve(address(foldCaptiveStaking), type(uint256).max); - - foldCaptiveStaking.deposit(500 ether, 500 ether, 0); - - vm.stopPrank(); + function testWithdrawalCooldown() public { + testAddLiquidity(); - // User 1 withdraws vm.startPrank(User01); (uint128 liq,,,) = foldCaptiveStaking.balances(User01); - foldCaptiveStaking.withdraw(liq / 2); - - (uint128 amount,,,) = foldCaptiveStaking.balances(User01); - assertEq(amount, liq / 2); - vm.stopPrank(); + // Attempt to withdraw before cooldown period + vm.expectRevert(WithdrawalCooldownPeriodNotMet.selector); + foldCaptiveStaking.withdraw(liq / 2); - // User 2 withdraws - vm.startPrank(User02); + // Simulate passage of cooldown period + vm.warp(block.timestamp + 14 days); - (liq,,,) = foldCaptiveStaking.balances(User02); + // Withdraw after cooldown period foldCaptiveStaking.withdraw(liq / 2); - - (amount,,,) = foldCaptiveStaking.balances(User02); + (uint128 amount,,,) = foldCaptiveStaking.balances(User01); assertEq(amount, liq / 2); vm.stopPrank(); From c5a79643f82bff2d11e3284e0f0cbd960c44c554 Mon Sep 17 00:00:00 2001 From: Sandy Bradley Date: Mon, 26 Aug 2024 21:31:25 +0200 Subject: [PATCH 3/3] feat: safeTransfers (#19) --- src/FoldCaptiveStaking.sol | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/FoldCaptiveStaking.sol b/src/FoldCaptiveStaking.sol index 855e702..fdbfec0 100644 --- a/src/FoldCaptiveStaking.sol +++ b/src/FoldCaptiveStaking.sol @@ -9,6 +9,7 @@ import {IUniswapV3Pool} from "./interfaces/IUniswapV3Pool.sol"; /// Libraries import {TickMath} from "./libraries/TickMath.sol"; +import {SafeTransferLib} from "lib/solmate/src/utils/SafeTransferLib.sol"; /// contracts import {ERC20} from "lib/solmate/src/tokens/ERC20.sol"; @@ -19,6 +20,9 @@ import {WETH} from "lib/solmate/src/tokens/WETH.sol"; /// @title FoldCaptiveStaking /// @notice Staking contract for managing FOLD token liquidity on Uniswap V3 contract FoldCaptiveStaking is Owned(msg.sender) { + using SafeTransferLib for ERC20; + using SafeTransferLib for WETH; + /*////////////////////////////////////////////////////////////// INITIALIZATION //////////////////////////////////////////////////////////////*/ @@ -87,8 +91,8 @@ contract FoldCaptiveStaking is Owned(msg.sender) { deadline: block.timestamp + 1 minutes }); - token0.approve(address(positionManager), type(uint256).max); - token1.approve(address(positionManager), type(uint256).max); + token0.safeApprove(address(positionManager), type(uint256).max); + token1.safeApprove(address(positionManager), type(uint256).max); uint128 liquidity; (TOKEN_ID, liquidity,,) = positionManager.mint(params); @@ -198,16 +202,16 @@ contract FoldCaptiveStaking is Owned(msg.sender) { deadline: block.timestamp + 1 minutes }); - token0.transferFrom(msg.sender, address(this), amount0); - token1.transferFrom(msg.sender, address(this), amount1); + token0.safeTransferFrom(msg.sender, address(this), amount0); + token1.safeTransferFrom(msg.sender, address(this), amount1); (uint128 liquidity, uint256 actualAmount0, uint256 actualAmount1) = positionManager.increaseLiquidity(params); if (actualAmount0 < amount0) { - token0.transfer(msg.sender, amount0 - actualAmount0); + token0.safeTransfer(msg.sender, amount0 - actualAmount0); } if (actualAmount1 < amount1) { - token1.transfer(msg.sender, amount1 - actualAmount1); + token1.safeTransfer(msg.sender, amount1 - actualAmount1); } balances[msg.sender].amount += liquidity; @@ -243,8 +247,8 @@ contract FoldCaptiveStaking is Owned(msg.sender) { (uint128 liquidity, uint256 actualAmount0, uint256 actualAmount1) = positionManager.increaseLiquidity(params); - token0.transfer(msg.sender, fee0Owed - actualAmount0); - token1.transfer(msg.sender, fee1Owed - actualAmount1); + token0.safeTransfer(msg.sender, fee0Owed - actualAmount0); + token1.safeTransfer(msg.sender, fee1Owed - actualAmount1); balances[msg.sender].token0FeeDebt = uint128(token0FeesPerLiquidity); balances[msg.sender].token1FeeDebt = uint128(token1FeesPerLiquidity); @@ -264,8 +268,8 @@ contract FoldCaptiveStaking is Owned(msg.sender) { uint256 fee1Owed = (token1FeesPerLiquidity - balances[msg.sender].token1FeeDebt) * balances[msg.sender].amount / liquidityUnderManagement; - token0.transfer(msg.sender, fee0Owed); - token1.transfer(msg.sender, fee1Owed); + token0.safeTransfer(msg.sender, fee0Owed); + token1.safeTransfer(msg.sender, fee1Owed); balances[msg.sender].token0FeeDebt = uint128(token0FeesPerLiquidity); balances[msg.sender].token1FeeDebt = uint128(token1FeesPerLiquidity); @@ -278,7 +282,7 @@ contract FoldCaptiveStaking is Owned(msg.sender) { uint256 rewardsOwed = (rewardsPerLiquidity - balances[msg.sender].rewardDebt) * balances[msg.sender].amount / liquidityUnderManagement; - WETH9.transfer(msg.sender, rewardsOwed); + WETH9.safeTransfer(msg.sender, rewardsOwed); balances[msg.sender].rewardDebt = uint128(rewardsPerLiquidity); @@ -320,8 +324,8 @@ contract FoldCaptiveStaking is Owned(msg.sender) { revert WithdrawFailed(); } - token0.transfer(msg.sender, amount0); - token1.transfer(msg.sender, amount1); + token0.safeTransfer(msg.sender, amount0); + token1.safeTransfer(msg.sender, amount1); emit Withdraw(msg.sender, liquidity); } @@ -385,8 +389,8 @@ contract FoldCaptiveStaking is Owned(msg.sender) { revert WithdrawFailed(); } - token0.transfer(owner, amount0); - token1.transfer(owner, amount1); + token0.safeTransfer(owner, amount0); + token1.safeTransfer(owner, amount1); emit InsuranceClaimed(owner, amount0, amount1); }