Skip to content

Commit

Permalink
Happy path integration tests
Browse files Browse the repository at this point in the history
  • Loading branch information
alexkeating committed Feb 15, 2024
1 parent 02d9ffa commit fbb0a82
Show file tree
Hide file tree
Showing 14 changed files with 549 additions and 58 deletions.
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/openzeppelin/openzeppelin-contracts
[submodule "lib/v3-core"]
path = lib/v3-core
url = https://github.com/uniswap/v3-core
[submodule "lib/v3-periphery"]
path = lib/v3-periphery
url = https://github.com/uniswap/v3-periphery
6 changes: 5 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
evm_version = "paris"
optimizer = true
optimizer_runs = 10_000_000
remappings = ["openzeppelin/=lib/openzeppelin-contracts/contracts"]
remappings = [
"openzeppelin/=lib/openzeppelin-contracts/contracts",
"uniswap-periphery/=lib/v3-periphery/contracts",
"@uniswap/v3-core=lib/v3-core",
]
solc_version = "0.8.23"
verbosity = 3

Expand Down
2 changes: 1 addition & 1 deletion lib/forge-std
1 change: 1 addition & 0 deletions lib/v3-core
Submodule v3-core added at e3589b
1 change: 1 addition & 0 deletions lib/v3-periphery
Submodule v3-periphery added at 697c24
8 changes: 8 additions & 0 deletions src/interfaces/IUniswapV3FactoryOwnerActions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,12 @@ interface IUniswapV3FactoryOwnerActions {
/// @param tickSpacing The spacing between ticks to be enforced for all pools created with the
/// given fee amount
function enableFeeAmount(uint24 fee, int24 tickSpacing) external;

/// @notice Returns the tick spacing for a given fee amount, if enabled, or 0 if not enabled
/// @dev A fee amount can never be removed, so this value should be hard coded or cached in the
/// calling context
/// @param fee The enabled fee, denominated in hundredths of a bip. Returns 0 in case of unenabled
/// fee
/// @return The tick spacing
function feeAmountTickSpacing(uint24 fee) external view returns (int24);
}
265 changes: 264 additions & 1 deletion test/UniStaker.integration.t.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.23;

import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
import {Test, console2} from "forge-std/Test.sol";
import {Deploy} from "script/Deploy.s.sol";
import {DeployInput} from "script/DeployInput.sol";
Expand All @@ -10,6 +11,10 @@ import {UniStaker} from "src/UniStaker.sol";
import {ProposalTest} from "test/helpers/ProposalTest.sol";
import {IUniswapV3FactoryOwnerActions} from "src/interfaces/IUniswapV3FactoryOwnerActions.sol";
import {IUniswapPool} from "test/helpers/interfaces/IUniswapPool.sol";
import {IUniswapV3PoolOwnerActions} from "src/interfaces/IUniswapV3PoolOwnerActions.sol";
import {IUniswapPool} from "test/helpers/interfaces/IUniswapPool.sol";
import {PercentAssertions} from "test/helpers/PercentAssertions.sol";
import {IntegrationTest} from "test/helpers/IntegrationTest.sol";

contract DeployScriptTest is Test, DeployInput {
function setUp() public {
Expand All @@ -34,7 +39,7 @@ contract DeployScriptTest is Test, DeployInput {
}
}

contract Propose is ProposalTest {
contract Propose is IntegrationTest {
function testFork_CorrectlyPassAndExecuteProposal() public {
IUniswapV3FactoryOwnerActions factory =
IUniswapV3FactoryOwnerActions(UNISWAP_V3_FACTORY_ADDRESS);
Expand Down Expand Up @@ -68,4 +73,262 @@ contract Propose is ProposalTest {
assertEq(newDaiWethFeeProtocol, 10 + (10 << 4));
assertEq(newDaiUsdcFeeProtocol, 10 + (10 << 4));
}

function testForkFuzz_CorrectlyEnableFeeAmountAfterProposalIsExecuted(
uint24 _fee,
int24 _tickSpacing
) public {
// These bounds are specified in the pool contract
// https://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/UniswapV3Factory.sol#L61
_fee = uint24(bound(_fee, 0, 999_999));
_tickSpacing = int24(bound(_tickSpacing, 1, 16_383));

_setupProposals();

vm.prank(UNISWAP_GOVERNOR_TIMELOCK);
v3FactoryOwner.enableFeeAmount(_fee, _tickSpacing);
IUniswapV3FactoryOwnerActions factory =
IUniswapV3FactoryOwnerActions(UNISWAP_V3_FACTORY_ADDRESS);

int24 tickSpacing = factory.feeAmountTickSpacing(_fee);
assertEq(tickSpacing, _tickSpacing, "Tick spacing is incorrect for the set fee");
}

function testForkFuzz_RevertIf_EnableFeeAmountFeeIsTooHighAfterProposalIsExecuted(
uint24 _fee,
int24 _tickSpacing
) public {
_fee = uint24(bound(_fee, 1_000_000, type(uint24).max));

_setupProposals();

vm.prank(UNISWAP_GOVERNOR_TIMELOCK);
vm.expectRevert(bytes(""));
v3FactoryOwner.enableFeeAmount(_fee, _tickSpacing);
}

function testForkFuzz_RevertIf_EnableFeeAmountTickSpacingIsTooHighAfterProposalIsExecuted(
uint24 _fee,
int24 _tickSpacing
) public {
_tickSpacing = int24(bound(_tickSpacing, 16_383, type(int24).max));

_setupProposals();

vm.prank(UNISWAP_GOVERNOR_TIMELOCK);
vm.expectRevert(bytes(""));
v3FactoryOwner.enableFeeAmount(_fee, _tickSpacing);
}

function testForkFuzz_RevertIf_EnableFeeAmountTickSpacingIsTooLowAfterProposalIsExecuted(
uint24 _fee,
int24 _tickSpacing
) public {
_tickSpacing = int24(bound(_tickSpacing, type(int24).min, 0));

_setupProposals();

vm.prank(UNISWAP_GOVERNOR_TIMELOCK);
vm.expectRevert(bytes(""));
v3FactoryOwner.enableFeeAmount(_fee, _tickSpacing);
}

function testForkFuzz_CorrectlySwapWethAndNotifyRewardAfterProposalIsExecuted(uint128 _amount)
public
{
IERC20 weth = IERC20(payable(WETH_ADDRESS));
IUniswapPool daiWethPool = IUniswapPool(DAI_WETH_3000_POOL);

// Amount should be high enough to generate fees
_amount = uint128(bound(_amount, 1e18, 1_000_000e18));
uint256 totalWETH = _amount + PAYOUT_AMOUNT;
vm.deal(address(this), totalWETH);
deal(address(weth), address(this), totalWETH);

weth.approve(address(UNISWAP_V3_SWAP_ROUTER), totalWETH);
weth.approve(address(v3FactoryOwner), totalWETH);

_setupProposals();
_swapTokens(WETH_ADDRESS, DAI_ADDRESS, _amount);

(uint128 token0Fees, uint128 token1Fees) = daiWethPool.protocolFees();

// We subtract 1 to make sure the requested amount is less then the actual fees
v3FactoryOwner.claimFees(
IUniswapV3PoolOwnerActions(DAI_WETH_3000_POOL), address(this), token0Fees, token1Fees - 1
);

uint256 balance = IERC20(WETH_ADDRESS).balanceOf(address(this));
assertEq(balance, uint256(token1Fees - 1));
assertEq(0, token0Fees);
}

function testForkFuzz_CorrectlySwapDaiAndNotifyRewardAfterProposalIsExecuted(uint128 _amount)
public
{
// Amount should be high enough to generate fees
IERC20 weth = IERC20(payable(WETH_ADDRESS));
IERC20 dai = IERC20(payable(DAI_ADDRESS));
IUniswapPool daiWethPool = IUniswapPool(DAI_WETH_3000_POOL);

_amount = uint128(bound(_amount, 1e18, 1_000_000e18));
uint256 totalDai = _amount;

vm.deal(address(this), PAYOUT_AMOUNT);
deal(address(dai), address(this), totalDai, true);
deal(address(weth), address(this), PAYOUT_AMOUNT);

dai.approve(address(UNISWAP_V3_SWAP_ROUTER), totalDai);
weth.approve(address(v3FactoryOwner), PAYOUT_AMOUNT);

_setupProposals();
_swapTokens(DAI_ADDRESS, WETH_ADDRESS, _amount);

(uint128 token0Fees, uint128 token1Fees) = daiWethPool.protocolFees();
// We subtract 1 to make sure the requested amount is less then the actual fees
v3FactoryOwner.claimFees(
IUniswapV3PoolOwnerActions(DAI_WETH_3000_POOL), address(this), token0Fees - 1, token1Fees
);

uint256 balance = IERC20(DAI_ADDRESS).balanceOf(address(this));
assertEq(0, token1Fees);
assertEq(balance, uint256(token0Fees - 1));
}

function testForkFuzz_CorrectlySwapDaiAndWETHThenNotifyRewardAfterProposalIsExecuted(
uint128 _amountDai,
uint128 _amountWeth
) public {
IERC20 weth = IERC20(payable(WETH_ADDRESS));
IERC20 dai = IERC20(payable(DAI_ADDRESS));
IUniswapPool daiWethPool = IUniswapPool(DAI_WETH_3000_POOL);

// Amount should be high enough to generate fees
_amountDai = uint128(bound(_amountDai, 1e18, 1_000_000e18));
_amountWeth = uint128(bound(_amountWeth, 1e18, 1_000_000e18));
uint256 totalDai = _amountDai;
uint256 totalWeth = _amountWeth + PAYOUT_AMOUNT;

vm.deal(address(this), totalWeth);
deal(address(dai), address(this), totalDai, true);
deal(address(weth), address(this), totalWeth);

dai.approve(address(UNISWAP_V3_SWAP_ROUTER), totalDai);
weth.approve(address(UNISWAP_V3_SWAP_ROUTER), totalWeth);
weth.approve(address(v3FactoryOwner), PAYOUT_AMOUNT);

_setupProposals();
_swapTokens(DAI_ADDRESS, WETH_ADDRESS, totalDai);
_swapTokens(WETH_ADDRESS, DAI_ADDRESS, _amountWeth);

uint256 originalDaiBalance = IERC20(DAI_ADDRESS).balanceOf(address(this));
uint256 originalWethBalance = IERC20(WETH_ADDRESS).balanceOf(address(this)) - PAYOUT_AMOUNT;

(uint128 token0Fees, uint128 token1Fees) = daiWethPool.protocolFees();
// We subtract 1 to make sure the requested amount is less then the actual fees
v3FactoryOwner.claimFees(
IUniswapV3PoolOwnerActions(DAI_WETH_3000_POOL), address(this), token0Fees - 1, token1Fees - 1
);

uint256 daiBalance = IERC20(DAI_ADDRESS).balanceOf(address(this));
uint256 wethBalance = IERC20(WETH_ADDRESS).balanceOf(address(this));
assertEq(wethBalance - originalWethBalance, token1Fees - 1);
assertEq(daiBalance - originalDaiBalance, uint256(token0Fees - 1));
}
}

contract Stake is IntegrationTest, PercentAssertions {
function testForkFuzz_CorrectlyStakeAndEarnRewardsAfterFullDuration(
address _depositor,
uint256 _amount,
address _delegatee,
uint128 _swapAmount
) public {
vm.assume(_depositor != address(0) && _delegatee != address(0) && _amount != 0);
_setupProposals();
_notifyRewards(_swapAmount);
_amount = _dealStakingToken(_depositor, _amount);

vm.prank(_depositor);
uniStaker.stake(_amount, _delegatee);

_jumpAheadByPercentOfRewardDuration(101);
assertLteWithinOnePercent(uniStaker.unclaimedReward(address(_depositor)), PAYOUT_AMOUNT);
}

function testForkFuzz_CorrectlyStakeAndClaimRewardsAfterFullDuration(
address _depositor,
uint256 _amount,
address _delegatee,
uint128 _swapAmount
) public {
vm.assume(_depositor != address(0) && _delegatee != address(0) && _amount != 0);
_setupProposals();
_notifyRewards(_swapAmount);
_amount = _dealStakingToken(_depositor, _amount);

vm.prank(_depositor);
uniStaker.stake(_amount, _delegatee);

_jumpAheadByPercentOfRewardDuration(101);
IERC20 weth = IERC20(WETH_ADDRESS);
uint256 oldBalance = weth.balanceOf(_depositor);

vm.prank(_depositor);
uniStaker.claimReward();

uint256 newBalance = weth.balanceOf(_depositor);
assertLteWithinOnePercent(newBalance - oldBalance, PAYOUT_AMOUNT);
assertEq(uniStaker.unclaimedReward(address(_depositor)), 0);
}

function testForkFuzz_CorrectlyStakeAndEarnRewardsAfterPartialDuration(
address _depositor,
uint256 _amount,
address _delegatee,
uint128 _swapAmount,
uint256 _percentDuration
) public {
vm.assume(_depositor != address(0) && _delegatee != address(0) && _amount != 0);
_percentDuration = bound(_percentDuration, 0, 100);
_setupProposals();
_notifyRewards(_swapAmount);
_amount = _dealStakingToken(_depositor, _amount);

vm.prank(_depositor);
uniStaker.stake(_amount, _delegatee);

_jumpAheadByPercentOfRewardDuration(100 - _percentDuration);
assertLteWithinOnePercent(
uniStaker.unclaimedReward(address(_depositor)),
_percentOf(PAYOUT_AMOUNT, 100 - _percentDuration)
);
}

function testForkFuzz_CorrectlyStakeMoreAndEarnRewardsAfterFullDuration(
address _depositor,
uint256 _initialAmount,
uint256 _additionalAmount,
address _delegatee,
uint128 _swapAmount,
uint256 _percentDuration
) public {
vm.assume(_depositor != address(0) && _delegatee != address(0));
_setupProposals();
_notifyRewards(_swapAmount);
_initialAmount = _dealStakingToken(_depositor, _initialAmount);
_percentDuration = bound(_percentDuration, 0, 100);

vm.prank(_depositor);
UniStaker.DepositIdentifier _depositId = uniStaker.stake(_initialAmount, _delegatee);

_jumpAheadByPercentOfRewardDuration(100 - _percentDuration);

_additionalAmount = _dealStakingToken(_depositor, _additionalAmount);
vm.prank(_depositor);
uniStaker.stakeMore(_depositId, _additionalAmount);

_jumpAheadByPercentOfRewardDuration(_percentDuration);
assertLteWithinOnePercent(uniStaker.unclaimedReward(address(_depositor)), PAYOUT_AMOUNT);
}
}
Loading

0 comments on commit fbb0a82

Please sign in to comment.