Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial happy path integration tests #56

Merged
merged 16 commits into from
Feb 22, 2024
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ jobs:
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

# https://twitter.com/PaulRBerg/status/1611116650664796166
- name: Generate fuzz seed with 1 week TTL
wildmolasses marked this conversation as resolved.
Show resolved Hide resolved
run: >
echo "FOUNDRY_FUZZ_SEED=$(
echo $(($EPOCHSECONDS - $EPOCHSECONDS % 604800))
Copy link
Collaborator

Choose a reason for hiding this comment

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

i lean towards a 1 day TTL instead, so that different fuzz runs can get us feedback quicker. but here, it's a big nit, since we're soon to be code complete

)" >> $GITHUB_ENV

- name: Run tests
run: forge test

Expand All @@ -44,6 +51,13 @@ jobs:
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

# https://twitter.com/PaulRBerg/status/1611116650664796166
- name: Recycle the fuzz seed from the test run
run: >
echo "FOUNDRY_FUZZ_SEED=$(
echo $(($EPOCHSECONDS - $EPOCHSECONDS % 604800))
)" >> $GITHUB_ENV

- name: Run coverage
run: forge coverage --report summary --report lcov

Expand Down
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
7 changes: 6 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
[profile.default]
evm_version = "paris"
fuzz = { seed = "1" }
Copy link
Collaborator

Choose a reason for hiding this comment

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

this implies a static fuzz seed will be used in local dev, right? I don't think that's what we want, or do i have the wrong idea?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes exactly, typically we add this locally as well when adding it to the CI. It all depends on what we want the default to be when running tests locally. I am happy to remove if we would rather not have it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

prefer no default seed

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);
}
270 changes: 268 additions & 2 deletions test/UniStaker.integration.t.sol
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
// 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";

import {V3FactoryOwner} from "src/V3FactoryOwner.sol";
import {UniStaker} from "src/UniStaker.sol";
import {ProposalTest} from "test/helpers/ProposalTest.sol";
import {IUniswapV3FactoryOwnerActions} from "src/interfaces/IUniswapV3FactoryOwnerActions.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 +37,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 +71,267 @@ 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));

IUniswapV3FactoryOwnerActions factory =
IUniswapV3FactoryOwnerActions(UNISWAP_V3_FACTORY_ADDRESS);
int24 oldTickSpacing = factory.feeAmountTickSpacing(_fee);
// If the tick spacing is above 0 it will revert
vm.assume(oldTickSpacing == 0);

_passQueueAndExecuteProposals();
vm.prank(UNISWAP_GOVERNOR_TIMELOCK);
v3FactoryOwner.enableFeeAmount(_fee, _tickSpacing);

int24 newTickSpacing = factory.feeAmountTickSpacing(_fee);
assertEq(newTickSpacing, _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));

_passQueueAndExecuteProposals();

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));

_passQueueAndExecuteProposals();

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));

_passQueueAndExecuteProposals();

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);

_passQueueAndExecuteProposals();
_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
{
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
_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);

_passQueueAndExecuteProposals();
_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);

_passQueueAndExecuteProposals();
_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);
_passQueueAndExecuteProposals();
_swapAndClaimFees(_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);
// Make sure depositor is not UniStaker
vm.assume(_depositor != 0xE2307e3710d108ceC7a4722a020a050681c835b3);
_passQueueAndExecuteProposals();
_swapAndClaimFees(_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);
_passQueueAndExecuteProposals();
_swapAndClaimFees(_swapAmount);
_amount = _dealStakingToken(_depositor, _amount);

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

_jumpAheadByPercentOfRewardDuration(100 - _percentDuration);
apbendi marked this conversation as resolved.
Show resolved Hide resolved
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));
_passQueueAndExecuteProposals();
_swapAndClaimFees(_swapAmount);
_initialAmount = _dealStakingToken(_depositor, _initialAmount);
_percentDuration = bound(_percentDuration, 0, 100);

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

_jumpAheadByPercentOfRewardDuration(_percentDuration);

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

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