Skip to content

Commit

Permalink
feat: support for native tokens in increase staked liq (#156)
Browse files Browse the repository at this point in the history
* feat: handle native tokens in increasestakedliq

* refactor: add receive to clgauge, to enable eth refunds

* test: add coverage for increaseliq with native tokens

* test: add asserts for weth

* fix: always refund eth

* test: eth refunds when no native token is used

* refactor: use transferhelper for transfers

* fix: avoid giving allowances with native token

* refactor: use transferhelper for approve, remove safeerc20

* refactor: early revert with supports payable check
  • Loading branch information
airtoonricardo authored Apr 9, 2024
1 parent 069e602 commit 4fe6f62
Show file tree
Hide file tree
Showing 71 changed files with 437 additions and 93 deletions.
88 changes: 66 additions & 22 deletions contracts/gauge/CLGauge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ pragma abicoder v2;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/ERC721Holder.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
import {ICLGauge} from "contracts/gauge/interfaces/ICLGauge.sol";
import {ICLGaugeFactory} from "contracts/gauge/interfaces/ICLGaugeFactory.sol";
import {IVoter} from "contracts/core/interfaces/IVoter.sol";
import {ICLPool} from "contracts/core/interfaces/ICLPool.sol";
import {TransferHelper} from "contracts/periphery/libraries/TransferHelper.sol";
import {INonfungiblePositionManager} from "contracts/periphery/interfaces/INonfungiblePositionManager.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {EnumerableSet} from "contracts/libraries/EnumerableSet.sol";
Expand All @@ -20,7 +20,6 @@ import {IReward} from "contracts/gauge/interfaces/IReward.sol";

contract CLGauge is ICLGauge, ERC721Holder, ReentrancyGuard {
using EnumerableSet for EnumerableSet.UintSet;
using SafeERC20 for IERC20;
using SafeCast for uint128;

/// @inheritdoc ICLGauge
Expand Down Expand Up @@ -58,6 +57,8 @@ contract CLGauge is ICLGauge, ERC721Holder, ReentrancyGuard {
/// @inheritdoc ICLGauge
uint256 public override fees1;
/// @inheritdoc ICLGauge
address public override WETH9;
/// @inheritdoc ICLGauge
address public override token0;
/// @inheritdoc ICLGauge
address public override token1;
Expand All @@ -66,6 +67,8 @@ contract CLGauge is ICLGauge, ERC721Holder, ReentrancyGuard {

/// @inheritdoc ICLGauge
bool public override isPool;
/// @inheritdoc ICLGauge
bool public override supportsPayable;

/// @inheritdoc ICLGauge
function initialize(
Expand All @@ -86,10 +89,17 @@ contract CLGauge is ICLGauge, ERC721Holder, ReentrancyGuard {
rewardToken = _rewardToken;
voter = IVoter(_voter);
nft = INonfungiblePositionManager(_nft);
address _weth = nft.WETH9();
WETH9 = _weth;
token0 = _token0;
token1 = _token1;
tickSpacing = _tickSpacing;
isPool = _isPool;
supportsPayable = _token0 == _weth || _token1 == _weth;
}

receive() external payable {
require(msg.sender == address(nft), "NNFT");
}

// updates the claimable rewards and lastUpdateTime for tokenId
Expand Down Expand Up @@ -164,7 +174,7 @@ contract CLGauge is ICLGauge, ERC721Holder, ReentrancyGuard {

if (reward > 0) {
delete rewards[tokenId];
IERC20(rewardToken).safeTransfer(owner, reward);
TransferHelper.safeTransfer(rewardToken, owner, reward);
emit ClaimRewards(owner, reward);
}
}
Expand Down Expand Up @@ -237,21 +247,22 @@ contract CLGauge is ICLGauge, ERC721Holder, ReentrancyGuard {
uint256 amount0Min,
uint256 amount1Min,
uint256 deadline
) external override nonReentrant returns (uint128 liquidity, uint256 amount0, uint256 amount1) {
) external payable override nonReentrant returns (uint128 liquidity, uint256 amount0, uint256 amount1) {
require(_stakes[msg.sender].contains(tokenId), "NA");
require(voter.isAlive(address(this)), "GK");
require(msg.value == 0 || supportsPayable, "NP");

// NFT manager will send these tokens to the pool
IERC20(token0).safeIncreaseAllowance(address(nft), amount0Desired);
IERC20(token1).safeIncreaseAllowance(address(nft), amount1Desired);

IERC20(token0).safeTransferFrom(msg.sender, address(this), amount0Desired);
IERC20(token1).safeTransferFrom(msg.sender, address(this), amount1Desired);
_transferTokensFromSender({
_token0: token0,
_token1: token1,
amount0Desired: amount0Desired,
amount1Desired: amount1Desired
});

(,,,,, int24 tickLower, int24 tickUpper,,,,,) = nft.positions(tokenId);
_updateRewards(tokenId, tickLower, tickUpper);

(liquidity, amount0, amount1) = nft.increaseLiquidity(
(liquidity, amount0, amount1) = nft.increaseLiquidity{value: msg.value}(
INonfungiblePositionManager.IncreaseLiquidityParams({
tokenId: tokenId,
amount0Desired: amount0Desired,
Expand All @@ -263,18 +274,51 @@ contract CLGauge is ICLGauge, ERC721Holder, ReentrancyGuard {
);

pool.stake(liquidity.toInt128(), tickLower, tickUpper, false);
_refundSurplusTokens({_token0: token0, _token1: token1});
}

uint256 amount0Surplus = amount0Desired - amount0;
uint256 amount1Surplus = amount1Desired - amount1;

if (amount0Surplus > 0) {
IERC20(token0).safeTransfer(msg.sender, amount0Surplus);
}
if (amount1Surplus > 0) {
IERC20(token1).safeTransfer(msg.sender, amount1Surplus);
/// @notice Transfers tokens from the caller to the Gauge prior to increasing Position size
/// @dev Checks if one of the tokens to be deposited is WETH and if its deposit
/// should be performed with Native Tokens.
/// @param _token0 Address of token0 to be deposited
/// @param _token1 Address of token1 to be deposited
/// @param amount0Desired Amount of token0 to be deposited into the position
/// @param amount1Desired Amount of token1 to be deposited into the position
function _transferTokensFromSender(address _token0, address _token1, uint256 amount0Desired, uint256 amount1Desired)
internal
{
/// @dev Handle native Tokens
if (msg.value > 0) {
if (_token0 == WETH9) {
TransferHelper.safeApprove(_token1, address(nft), amount1Desired);
TransferHelper.safeTransferFrom(_token1, msg.sender, address(this), amount1Desired);
} else {
TransferHelper.safeApprove(_token0, address(nft), amount0Desired);
TransferHelper.safeTransferFrom(_token0, msg.sender, address(this), amount0Desired);
}
} else {
// NFT manager will send these tokens to the pool
TransferHelper.safeApprove(_token0, address(nft), amount0Desired);
TransferHelper.safeApprove(_token1, address(nft), amount1Desired);
TransferHelper.safeTransferFrom(_token0, msg.sender, address(this), amount0Desired);
TransferHelper.safeTransferFrom(_token1, msg.sender, address(this), amount1Desired);
}
}

/// @notice After increasing Position size, transfers any surplus tokens back to caller
/// @dev Also handles native token refunds
/// @param _token0 Address of the token0 to be deposited
/// @param _token1 Address of the token1 to be deposited
function _refundSurplusTokens(address _token0, address _token1) internal {
uint256 balance = IERC20(_token0).balanceOf(address(this));
if (balance > 0) TransferHelper.safeTransfer(_token0, msg.sender, balance);

balance = IERC20(_token1).balanceOf(address(this));
if (balance > 0) TransferHelper.safeTransfer(_token1, msg.sender, balance);

if (address(this).balance > 0) TransferHelper.safeTransferETH(msg.sender, address(this).balance);
}

/// @inheritdoc ICLGauge
function decreaseStakedLiquidity(
uint256 tokenId,
Expand Down Expand Up @@ -363,7 +407,7 @@ contract CLGauge is ICLGauge, ERC721Holder, ReentrancyGuard {
pool.updateRewardsGrowthGlobal();
uint256 nextPeriodFinish = timestamp + timeUntilNext;

IERC20(rewardToken).safeTransferFrom(_sender, address(this), _amount);
TransferHelper.safeTransferFrom(rewardToken, _sender, address(this), _amount);
// rolling over stuck rewards from previous epoch (if any)
_amount += pool.rollover();

Expand Down Expand Up @@ -398,14 +442,14 @@ contract CLGauge is ICLGauge, ERC721Holder, ReentrancyGuard {
address _token1 = token1;
if (_fees0 > VelodromeTimeLibrary.WEEK) {
fees0 = 0;
IERC20(_token0).safeIncreaseAllowance(feesVotingReward, _fees0);
TransferHelper.safeApprove(_token0, feesVotingReward, _fees0);
IReward(feesVotingReward).notifyRewardAmount(_token0, _fees0);
} else {
fees0 = _fees0;
}
if (_fees1 > VelodromeTimeLibrary.WEEK) {
fees1 = 0;
IERC20(_token1).safeIncreaseAllowance(feesVotingReward, _fees1);
TransferHelper.safeApprove(_token1, feesVotingReward, _fees1);
IReward(feesVotingReward).notifyRewardAmount(_token1, _fees1);
} else {
fees1 = _fees1;
Expand Down
8 changes: 7 additions & 1 deletion contracts/gauge/interfaces/ICLGauge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ interface ICLGauge {
/// @notice Cached amount of fees generated from the Pool linked to the Gauge of token1
function fees1() external view returns (uint256);

/// @notice Cached address of WETH9
function WETH9() external view returns (address);

/// @notice Cached address of token0, corresponding to token0 of the pool
function token0() external view returns (address);

Expand All @@ -67,6 +70,9 @@ interface ICLGauge {
/// @notice To provide compatibility support with the old voter
function isPool() external view returns (bool);

/// @notice Checks whether the gauge supports payments in Native tokens
function supportsPayable() external view returns (bool);

/// @notice Returns the rewardGrowthInside of the position at the last user action (deposit, withdraw, getReward)
/// @param tokenId The tokenId of the position
/// @return The rewardGrowthInside for the position
Expand Down Expand Up @@ -150,7 +156,7 @@ interface ICLGauge {
uint256 amount0Min,
uint256 amount1Min,
uint256 deadline
) external returns (uint128 liquidity, uint256 amount0, uint256 amount1);
) external payable returns (uint128 liquidity, uint256 amount0, uint256 amount1);

/// @notice Used to decrease liquidity of a staked position
/// @param tokenId The tokenId of the position
Expand Down
2 changes: 1 addition & 1 deletion test/BaseFixture.sol
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ abstract contract BaseFixture is Test, Constants, Events, PoolUtils {
if (rewardToken.allowance(_voter, _gauge) < _amount) {
rewardToken.approve(_gauge, _amount);
}
CLGauge(_gauge).notifyRewardAmount(_amount);
CLGauge(payable(_gauge)).notifyRewardAmount(_amount);
vm.stopPrank();
}

Expand Down
2 changes: 1 addition & 1 deletion test/fork/e2e/GaugeFlow.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ contract GaugeFlowTest is BaseForkFixture {
vm.prank(users.feeManager);
customUnstakedFeeModule.setCustomFee(address(pool), 10_000);

gauge = CLGauge(voter.createGauge({_poolFactory: address(poolFactory), _pool: address(pool)}));
gauge = CLGauge(payable(voter.createGauge({_poolFactory: address(poolFactory), _pool: address(pool)})));

feesVotingReward = gauge.feesVotingReward();

Expand Down
2 changes: 1 addition & 1 deletion test/fork/notifyRewardAmountWithoutClaim.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ contract NotifyRewardAmountWithoutClaimForkTest is BaseForkFixture {
vm.prank(users.feeManager);
customUnstakedFeeModule.setCustomFee(address(pool), 420);

gauge = CLGauge(voter.createGauge({_poolFactory: address(poolFactory), _pool: address(pool)}));
gauge = CLGauge(payable(voter.createGauge({_poolFactory: address(poolFactory), _pool: address(pool)})));
feesVotingReward = voter.gaugeToFees(address(gauge));

skipToNextEpoch(0);
Expand Down
2 changes: 1 addition & 1 deletion test/invariants/Setup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ contract SetupCL {
})
);

gauge = CLGauge(voter.gauges(address(pool)));
gauge = CLGauge(payable(voter.gauges(address(pool))));

hevm.prank(address(voter));
rewardToken.approve(address(gauge), 1000000000e18);
Expand Down
6 changes: 3 additions & 3 deletions test/unit/concrete/CLFactory/createPool.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ contract CreatePoolTest is CLFactoryTest {
});
assertEqUint(poolFactory.getSwapFee(pool), 500);

CLGauge gauge = CLGauge(voter.createGauge({_poolFactory: address(poolFactory), _pool: address(pool)}));
CLGauge gauge = CLGauge(payable(voter.createGauge({_poolFactory: address(poolFactory), _pool: address(pool)})));
address feesVotingReward = voter.gaugeToFees(address(gauge));
assertEq(address(gauge.pool()), address(pool));
assertEq(gauge.feesVotingReward(), address(feesVotingReward));
Expand All @@ -110,7 +110,7 @@ contract CreatePoolTest is CLFactoryTest {
});
assertEqUint(poolFactory.getSwapFee(pool), 3_000);

CLGauge gauge = CLGauge(voter.createGauge({_poolFactory: address(poolFactory), _pool: address(pool)}));
CLGauge gauge = CLGauge(payable(voter.createGauge({_poolFactory: address(poolFactory), _pool: address(pool)})));
address feesVotingReward = voter.gaugeToFees(address(gauge));
assertEq(address(gauge.pool()), address(pool));
assertEq(gauge.feesVotingReward(), address(feesVotingReward));
Expand All @@ -128,7 +128,7 @@ contract CreatePoolTest is CLFactoryTest {
});
assertEqUint(poolFactory.getSwapFee(pool), 10_000);

CLGauge gauge = CLGauge(voter.createGauge({_poolFactory: address(poolFactory), _pool: address(pool)}));
CLGauge gauge = CLGauge(payable(voter.createGauge({_poolFactory: address(poolFactory), _pool: address(pool)})));
address feesVotingReward = voter.gaugeToFees(address(gauge));
assertEq(address(gauge.pool()), address(pool));
assertEq(gauge.feesVotingReward(), address(feesVotingReward));
Expand Down
2 changes: 1 addition & 1 deletion test/unit/concrete/CLFactory/getUnstakedFee.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ contract GetUnstakedFeeTest is CLFactoryTest {
sqrtPriceX96: encodePriceSqrt(1, 1)
});

gauge = CLGauge(voter.createGauge({_poolFactory: address(poolFactory), _pool: address(pool)}));
gauge = CLGauge(payable(voter.createGauge({_poolFactory: address(poolFactory), _pool: address(pool)})));

assertEq(voter.isAlive(address(gauge)), true);
assertEq(uint256(poolFactory.getUnstakedFee(pool)), 100_000);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ contract LiquidityManagementBase is CLGaugeTest {
vm.prank(users.feeManager);
customUnstakedFeeModule.setCustomFee(address(pool), 420);

gauge = CLGauge(voter.createGauge({_poolFactory: address(poolFactory), _pool: address(pool)}));
gauge = CLGauge(payable(voter.createGauge({_poolFactory: address(poolFactory), _pool: address(pool)})));

deal({token: address(token0), to: users.bob, give: TOKEN_1 * 100});
deal({token: address(token1), to: users.bob, give: TOKEN_1 * 100});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ contract IncreaseStakedLiquidityTest is LiquidityManagementBase {
assertEq(aliceBalanceBefore0 - aliceBalanceAfter0, TOKEN_1);
assertEq(aliceBalanceBefore1 - aliceBalanceAfter1, TOKEN_1);

assertEq(token0.allowance(address(gauge), address(nft)), 0);
assertEq(token1.allowance(address(gauge), address(nft)), 0);

(,,,,,,, positionLiquidity,,,,) = nft.positions(tokenId);

assertEq(pool.stakedLiquidity(), TOKEN_1 * 2);
Expand Down
Loading

0 comments on commit 4fe6f62

Please sign in to comment.