From 2691c9371130ffa995f8c44b7236304cd31ae601 Mon Sep 17 00:00:00 2001 From: Trevor Richard Date: Sat, 14 Sep 2024 03:19:42 +0000 Subject: [PATCH] provide extended claimer interface for compounding swapper --- .../GenericSwapperHelper.sol | 101 ------- .../PrizeCompoundingSwapperHook.sol | 179 +++++++++--- .../PrizeCompoundingSwapperHookAndClaimer.sol | 255 ++++++++++++++++++ .../interfaces/IUniV3Oracle.sol | 28 ++ .../interfaces/IUniV3OracleFactory.sol | 10 + .../interfaces/IUniswapV3PoolImmutables.sol | 35 +++ .../interfaces/IUniswapV3Router.sol | 20 ++ .../PrizeCompoundingSwapperHook.t.sol | 208 -------------- ...rizeCompoundingSwapperHookAndClaimer.t.sol | 111 ++++++++ 9 files changed, 600 insertions(+), 347 deletions(-) delete mode 100644 src/prize-hooks/examples/prize-compounding-swapper/GenericSwapperHelper.sol create mode 100644 src/prize-hooks/examples/prize-compounding-swapper/PrizeCompoundingSwapperHookAndClaimer.sol create mode 100644 src/prize-hooks/examples/prize-compounding-swapper/interfaces/IUniV3Oracle.sol create mode 100644 src/prize-hooks/examples/prize-compounding-swapper/interfaces/IUniV3OracleFactory.sol create mode 100644 src/prize-hooks/examples/prize-compounding-swapper/interfaces/IUniswapV3PoolImmutables.sol create mode 100644 src/prize-hooks/examples/prize-compounding-swapper/interfaces/IUniswapV3Router.sol delete mode 100644 test/prize-hooks/examples/prize-compounding-swapper/PrizeCompoundingSwapperHook.t.sol create mode 100644 test/prize-hooks/examples/prize-compounding-swapper/PrizeCompoundingSwapperHookAndClaimer.t.sol diff --git a/src/prize-hooks/examples/prize-compounding-swapper/GenericSwapperHelper.sol b/src/prize-hooks/examples/prize-compounding-swapper/GenericSwapperHelper.sol deleted file mode 100644 index a3776a7..0000000 --- a/src/prize-hooks/examples/prize-compounding-swapper/GenericSwapperHelper.sol +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import { ISwapperFlashCallback } from "./interfaces/ISwapperFlashCallback.sol"; -import { ISwapper } from "./interfaces/ISwapper.sol"; -import { QuoteParams, QuotePair } from "./interfaces/IOracle.sol"; -import { IERC20 } from "openzeppelin-v5/token/ERC20/IERC20.sol"; -import { SafeERC20 } from "openzeppelin-v5/token/ERC20/utils/SafeERC20.sol"; - -/// @notice Structured flash info -/// @param profitRecipient The recipient of any profit (`tokenToBeneficiary` OR `tokenToSwap`) -/// @param tokenToSwap The token to pull from the swapper that will be used to swap -/// @param amountToSwap The amount of `tokenToSwap` to pull from the swapper -/// @param minTokenToSwapProfit The min amount of `tokenToSwap` profit that is expected to be sent to the -/// `profitRecipient` after the flash swap -/// @param minTokenToBeneficiaryProfit The min amount of `tokenToBeneficiary` profit that is expected to -/// be sent to the `profitRecipient` after the flash swap -/// @param approveTo The -struct FlashInfo { - address profitRecipient; - address tokenToSwap; - uint128 amountToSwap; - uint128 minTokenToSwapProfit; - uint256 minTokenToBeneficiaryProfit; - address approveTo; - address callTarget; - bytes callData; -} - -/// @title Generic Swapper Helper -/// @notice Provides a generic interface for performing single ERC20 flash swaps with a 0xSplits Swapper. -/// @author G9 Software Inc. -contract GenericSwapperHelper is ISwapperFlashCallback { - using SafeERC20 for IERC20; - - /// @notice Thrown when the min token profit is not met. - /// @param token The token expected as profit - /// @param actualProfit The actual profit that would have been received - /// @param minProfit The min profit set on the flash swap - error MinProfitNotMet(address token, uint256 actualProfit, uint256 minProfit); - - /// @notice Performs a flash swap on the swapper with the given flash info. - /// @dev If `flashInfo.profitRecipient` is left as the zero address, it will be set to the caller - /// @dev If `flashInfo.amountToSwap` is left as zero, it will be set to the swapper's balance - /// @dev If `flashInfo.approveTo` is left as the zero address, it will be set to the `flashInfo.callTarget` address - /// @param swapper The swapper to pull tokens from - /// @param flashInfo The flash info for the swap - function flashSwap(ISwapper swapper, FlashInfo memory flashInfo) external { - if (flashInfo.profitRecipient == address(0)) { - flashInfo.profitRecipient = msg.sender; - } - if (flashInfo.amountToSwap == 0) { - flashInfo.amountToSwap = uint128(IERC20(flashInfo.tokenToSwap).balanceOf(address(swapper))); - } - if (flashInfo.approveTo == address(0)) { - flashInfo.approveTo = flashInfo.callTarget; - } - QuoteParams[] memory quoteParams = new QuoteParams[](1); - quoteParams[0] = QuoteParams({ - quotePair: QuotePair({ - base: flashInfo.tokenToSwap, - quote: swapper.tokenToBeneficiary() - }), - baseAmount: flashInfo.amountToSwap, - data: "" - }); - swapper.flash(quoteParams, abi.encode(flashInfo)); - } - - /// @inheritdoc ISwapperFlashCallback - /// @dev Sends profit from both the `tokenToBeneficiary` and `tokenToSwap` to the `profitRecipient`. - function swapperFlashCallback(address tokenToBeneficiary, uint256 amountToBeneficiary, bytes calldata data) external { - FlashInfo memory flashInfo = abi.decode(data, (FlashInfo)); - IERC20(flashInfo.tokenToSwap).forceApprove(flashInfo.approveTo, flashInfo.amountToSwap); - (bool success, bytes memory returnData) = flashInfo.callTarget.call(flashInfo.callData); - if (!success) { - assembly { - revert(add(32, returnData), mload(returnData)) - } - } - - uint256 tokenToBeneficiaryBalance = IERC20(tokenToBeneficiary).balanceOf(address(this)); - IERC20(tokenToBeneficiary).forceApprove(msg.sender, amountToBeneficiary); - if (tokenToBeneficiaryBalance > amountToBeneficiary) { - uint256 profit = tokenToBeneficiaryBalance - amountToBeneficiary; - if (profit < flashInfo.minTokenToBeneficiaryProfit) { - revert MinProfitNotMet(tokenToBeneficiary, profit, flashInfo.minTokenToBeneficiaryProfit); - } - IERC20(tokenToBeneficiary).safeTransfer(flashInfo.profitRecipient, profit); - } - - uint256 tokenToSwapBalance = IERC20(flashInfo.tokenToSwap).balanceOf(address(this)); - if (tokenToSwapBalance > 0) { - if (tokenToSwapBalance < flashInfo.minTokenToSwapProfit) { - revert MinProfitNotMet(flashInfo.tokenToSwap, tokenToSwapBalance, flashInfo.minTokenToSwapProfit); - } - IERC20(flashInfo.tokenToSwap).safeTransfer(flashInfo.profitRecipient, tokenToSwapBalance); - } - } - -} \ No newline at end of file diff --git a/src/prize-hooks/examples/prize-compounding-swapper/PrizeCompoundingSwapperHook.sol b/src/prize-hooks/examples/prize-compounding-swapper/PrizeCompoundingSwapperHook.sol index e0dd06c..07f8f39 100644 --- a/src/prize-hooks/examples/prize-compounding-swapper/PrizeCompoundingSwapperHook.sol +++ b/src/prize-hooks/examples/prize-compounding-swapper/PrizeCompoundingSwapperHook.sol @@ -1,24 +1,35 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; +import { PrizeVault } from "pt-v5-vault/PrizeVault.sol"; +import { PrizePool } from "pt-v5-prize-pool/PrizePool.sol"; +import { NUMBER_OF_CANARY_TIERS } from "pt-v5-prize-pool/abstract/TieredLiquidityDistributor.sol"; import { IPrizeHooks } from "pt-v5-vault/interfaces/IPrizeHooks.sol"; -import { IERC4626 } from "openzeppelin-v5/interfaces/IERC4626.sol"; -import { IOracle, QuoteParams } from "./interfaces/IOracle.sol"; +import { IOracle, QuoteParams, QuotePair } from "./interfaces/IOracle.sol"; import { ISwapper } from "./interfaces/ISwapper.sol"; import { ISwapperFactory, CreateSwapperParams, OracleParams, CreateOracleParams, SetPairScaledOfferFactorParams } from "./interfaces/ISwapperFactory.sol"; +// The approximate denominator of the next daily prize size compared to the current daily prize +// size if the prize pool goes up in tiers. +uint256 constant NEXT_TIER_DAILY_PRIZE_DENOMINATOR = 4; + /// @notice Emitted when an account sets a new swapper. /// @param account The account setting the swapper -/// @param newTokenOut The token that the new swapper is set to swap to /// @param newSwapper The new swapper address /// @param previousSwapper The previous swapper address -event SetSwapper(address indexed account, address indexed newTokenOut, ISwapper indexed newSwapper, address previousSwapper); +event SetSwapper(address indexed account, address indexed newSwapper, address indexed previousSwapper); + +/// @notice Emitted when an account votes for a minimum desired prize size. +/// @param account The account voting +/// @param minDesiredPrizeSize The minimum desired prize size denominated in `compoundVault` tokens +event SetPrizeSizeVote(address indexed account, uint256 minDesiredPrizeSize); /// @title PoolTogether V5 - Prize Compounding Swapper Hook /// @notice Uses the 0xSplits Swapper contract factory to let users create their own swappers -/// that will receive any prizes won through this hook. External actors can then swap their -/// winnings back to the token or prize token they specify (assuming the base oracle has a -/// price for the token or underlying asset). +/// that will receive any prizes won through this hook. External actors can then swap winnings +/// to the `compoundVault` asset specified by this hook which is forwarded back to the winner. +/// The winner can also use this hook to vote on minimum prize sizes by specifying a prize amount +/// denominated in the `compoundVault` asset that is used to influence the prize pool tiers. /// @author G9 Software Inc. contract PrizeCompoundingSwapperHook is IPrizeHooks, IOracle { @@ -28,46 +39,107 @@ contract PrizeCompoundingSwapperHook is IPrizeHooks, IOracle { /// @notice The swapper factory to create new swappers from ISwapperFactory public immutable swapperFactory; - /// @notice The default scaled offer factor that is used for swaps (1e6 is 100%, over is premium to oracle, under is discount to oracle) - uint32 public immutable defaultScaledOfferFactor; + /// @notice The vault to compound to + PrizeVault public immutable compoundVault; + + /// @notice The scaled offer factor that is used for swaps (1e6 is 100%, over is premium to oracle, + /// under is discount to oracle) + uint32 public immutable scaledOfferFactor; + + /// @notice The prize pool associated with the prize vault + PrizePool public immutable prizePool; /// @notice Mapping of accounts to swappers. mapping(address account => address swapper) public swappers; + + /// @notice Mapping of accounts to minimum desired prize size denominated in `compoundVault` tokens. + mapping(address account => uint256 minDesiredPrizeSize) public prizeSizeVotes; + + /// @notice Thrown when the winning account votes against the current number of prize tiers. + /// @param currentNumTiers The current number of tiers + /// @param dailyPrizeSize The current daily prize size (denominated in `compoundVault` tokens) + /// @param minDesiredPrizeSize The minimum desired prize size (denominated in `compoundVault` tokens) + error VoteToLowerPrizeTiers(uint8 currentNumTiers, uint256 dailyPrizeSize, uint256 minDesiredPrizeSize); + + /// @notice Thrown when the winning account votes against going up in prize tiers, but is satisfied with staying + /// at the current number of tiers. + /// @param currentNumTiers The current number of tiers + /// @param dailyPrizeSize The current daily prize size (denominated in `compoundVault` tokens) + /// @param minDesiredPrizeSize The minimum desired prize size (denominated in `compoundVault` tokens) + error VoteToStayAtCurrentPrizeTiers(uint8 currentNumTiers, uint256 dailyPrizeSize, uint256 minDesiredPrizeSize); /// @notice Constructor - constructor(IOracle baseOracle_, ISwapperFactory swapperFactory_, uint32 defaultScaledOfferFactor_) { + /// @param baseOracle_ The oracle that will be used to find underlying asset pricing + /// @param swapperFactory_ The 0xSplits swapper factory + /// @param compoundVault_ The prize vault to compound prizes into + /// @param scaledOfferFactor_ Defines the discount (or premium) of swapper offers + constructor( + IOracle baseOracle_, + ISwapperFactory swapperFactory_, + PrizeVault compoundVault_, + uint32 scaledOfferFactor_ + ) { baseOracle = baseOracle_; swapperFactory = swapperFactory_; - defaultScaledOfferFactor = defaultScaledOfferFactor_; + compoundVault = compoundVault_; + scaledOfferFactor = scaledOfferFactor_; + prizePool = compoundVault_.prizePool(); } /// @notice Creates a new swapper for the caller and sets prizes to be redirected to their swapper. - /// @dev If the caller had a previous swapper set, it will transfer the ownership of that swapper to - /// the caller so they can recover any funds that are stuck. - /// @param _tokenOut The token to swap prizes to and send to the caller - /// @return The new swapper that is set for the caller - function setSwapper(address _tokenOut) external returns (ISwapper) { + /// @dev If the caller already has a swapper set, this call does nothing and returns the existing swapper. + /// @return The swapper that is set for the caller + function setSwapper() public returns (ISwapper) { + address _currentSwapper = swappers[msg.sender]; + if (_currentSwapper != address(0)) { + return ISwapper(_currentSwapper); + } else { + ISwapper _swapper = swapperFactory.createSwapper( + CreateSwapperParams({ + owner: address(this), + paused: false, + beneficiary: msg.sender, + tokenToBeneficiary: address(compoundVault), + oracleParams: OracleParams({ + oracle: this, + createOracleParams: CreateOracleParams(address(0), "") + }), + defaultScaledOfferFactor: scaledOfferFactor, + pairScaledOfferFactors: new SetPairScaledOfferFactorParams[](0) + }) + ); + emit SetSwapper(msg.sender, address(_swapper), _currentSwapper); + swappers[msg.sender] = address(_swapper); + return _swapper; + } + } + + /// @notice Sets the account minimum prize size vote. + /// @param minDesiredPrizeSize The account's minimum desired prize size denominated in `compoundVault` tokens + function setPrizeSizeVote(uint256 minDesiredPrizeSize) public { + prizeSizeVotes[msg.sender] = minDesiredPrizeSize; + emit SetPrizeSizeVote(msg.sender, minDesiredPrizeSize); + } + + /// @notice Sets the swapper and prize size vote for the account in one transaction. + /// @param minDesiredPrizeSize The account's minimum desired prize size denominated in `compoundVault` tokens + function setPrizeSizeVoteAndSwapper(uint256 minDesiredPrizeSize) external { + setPrizeSizeVote(minDesiredPrizeSize); + setSwapper(); + } + + /// @notice Transfers ownership of the caller's swapper to the caller's address. This is useful if + /// tokens get stuck in the caller's swapper that need to be recovered. + /// @dev Resets the caller's set swapper to the zero address. + /// @return The recovered swapper address (if any) + function removeAndRecoverSwapper() external returns (address) { address _previousSwapper = swappers[msg.sender]; if (_previousSwapper != address(0)) { ISwapper(_previousSwapper).transferOwnership(msg.sender); + delete swappers[msg.sender]; + emit SetSwapper(msg.sender, address(0), _previousSwapper); } - ISwapper _swapper = swapperFactory.createSwapper( - CreateSwapperParams({ - owner: address(this), - paused: false, - beneficiary: msg.sender, - tokenToBeneficiary: _tokenOut, - oracleParams: OracleParams({ - oracle: this, - createOracleParams: CreateOracleParams(address(0), "") - }), - defaultScaledOfferFactor: defaultScaledOfferFactor, - pairScaledOfferFactors: new SetPairScaledOfferFactorParams[](0) - }) - ); - swappers[msg.sender] = address(_swapper); - emit SetSwapper(msg.sender, _tokenOut, _swapper, _previousSwapper); - return _swapper; + return _previousSwapper; } /// @inheritdoc IOracle @@ -78,8 +150,8 @@ contract PrizeCompoundingSwapperHook is IPrizeHooks, IOracle { /// with old 0xSplits oracles. function getQuoteAmounts(QuoteParams[] memory quoteParams) public view returns (uint256[] memory) { for (uint256 i; i < quoteParams.length; i++) { - try IERC4626(quoteParams[i].quotePair.quote).asset() returns (address _asset) { - assert(IERC4626(quoteParams[i].quotePair.quote).convertToAssets(1) == 1); + try PrizeVault(quoteParams[i].quotePair.quote).asset() returns (address _asset) { + assert(PrizeVault(quoteParams[i].quotePair.quote).convertToAssets(1) == 1); quoteParams[i].quotePair.quote = _asset; } catch { // nothing @@ -99,10 +171,41 @@ contract PrizeCompoundingSwapperHook is IPrizeHooks, IOracle { /// @inheritdoc IPrizeHooks /// @dev Maps the winner address to their defined swapper contract and redirects the prize there. /// @dev If no swapper exists for the winner, the recipient will be set to the winner address. - function beforeClaimPrize(address winner, uint8, uint32, uint96, address) external view returns (address prizeRecipient, bytes memory data) { - address _swapper = swappers[winner]; + /// @dev If the winner has voted on a minimum prize size, this hook will revert certain canary claims + /// to influence the prize size based on the vote. + function beforeClaimPrize(address _winner, uint8 _tier, uint32, uint96, address) external view returns (address prizeRecipient, bytes memory data) { + uint256 _minDesiredPrizeSize = prizeSizeVotes[_winner]; + if (_minDesiredPrizeSize > 0) { + + // If the tier is a canary tier, determine if the account will vote against it + uint8 _numberOfTiers = prizePool.numberOfTiers(); + if (_tier >= _numberOfTiers - NUMBER_OF_CANARY_TIERS) { + uint128 _dailyPrizeSize = prizePool.getTierPrizeSize(_numberOfTiers - NUMBER_OF_CANARY_TIERS - 1); + QuoteParams[] memory _quoteParams = new QuoteParams[](1); + _quoteParams[0] = QuoteParams({ + quotePair: QuotePair({ + base: address(prizePool.prizeToken()), + quote: address(compoundVault) + }), + baseAmount: _dailyPrizeSize, + data: "" + }); + try this.getQuoteAmounts(_quoteParams) returns (uint256[] memory _convertedPrizeSize) { + if (_convertedPrizeSize[0] < _minDesiredPrizeSize) { + revert VoteToLowerPrizeTiers(_numberOfTiers, _convertedPrizeSize[0], _minDesiredPrizeSize); + } else if (_tier == _numberOfTiers - 1 && _convertedPrizeSize[0] / NEXT_TIER_DAILY_PRIZE_DENOMINATOR < _minDesiredPrizeSize) { + revert VoteToStayAtCurrentPrizeTiers(_numberOfTiers, _convertedPrizeSize[0], _minDesiredPrizeSize); + } + } catch { + // The oracle failed, so we will abstain voting for this claim + } + } + } + + // Set the prize recipient + address _swapper = swappers[_winner]; if (_swapper == address(0)) { - prizeRecipient = address(winner); + prizeRecipient = address(_winner); } else { prizeRecipient = _swapper; } diff --git a/src/prize-hooks/examples/prize-compounding-swapper/PrizeCompoundingSwapperHookAndClaimer.sol b/src/prize-hooks/examples/prize-compounding-swapper/PrizeCompoundingSwapperHookAndClaimer.sol new file mode 100644 index 0000000..ba51a18 --- /dev/null +++ b/src/prize-hooks/examples/prize-compounding-swapper/PrizeCompoundingSwapperHookAndClaimer.sol @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import { PrizeCompoundingSwapperHook, PrizeVault, PrizePool, ISwapper, ISwapperFactory, QuoteParams, QuotePair } from "./PrizeCompoundingSwapperHook.sol"; +import { ISwapperFlashCallback } from "./interfaces/ISwapperFlashCallback.sol"; +import { IUniV3Oracle } from "./interfaces/IUniV3Oracle.sol"; +import { IUniswapV3Router } from "./interfaces/IUniswapV3Router.sol"; +import { IUniswapV3PoolImmutables } from "./interfaces/IUniswapV3PoolImmutables.sol"; +import { IERC20 } from "openzeppelin-v5/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "openzeppelin-v5/token/ERC20/utils/SafeERC20.sol"; + +/// @title Prize Compounding Swapper Hook and Claimer +/// @notice Extension for the prize compounding swapper hook that adds helpful interfaces for flash +/// swaps and prize claims. +/// @author G9 Software Inc. +contract PrizeCompoundingSwapperHookAndClaimer is PrizeCompoundingSwapperHook, ISwapperFlashCallback { + using SafeERC20 for IERC20; + + // Constants + uint32 public constant FACTOR_DENOMINATOR = 1e6; // 100% + uint32 public constant MAX_CLAIM_REWARD_FACTOR = 0.1e6; // 10% (1e6 is 100%) + uint32 public constant CLAIM_FALLBACK_TIME_THRESHOLD = 0.75e6; // 75% (claim fallback will start after 75% of draw period) + + // Transient Storage Locations + bytes32 internal constant REENTRANT_STORAGE_KEY = 0x7cc960b12554423dd9d34a04ec8466d4c702b7fe05d9ad5b104f107dfb9d4674; // keccak256("PrizeCompoundingSwapperHookAndClaimer.Reentrant") + bytes32 internal constant UNIV3FEE_STORAGE_KEY = 0x6cd635abad6a5f9a689481baa48a89f282b7a6e2f80086c95886517083cfa753; // keccak256("PrizeCompoundingSwapperHookAndClaimer.UniV3Fee") + + /// @notice The uniswap router to use for compounding swaps + IUniswapV3Router public immutable uniV3Router; + + /// @notice Emitted when a claim reverts + /// @param vault The vault for which the claim failed + /// @param tier The tier for which the claim failed + /// @param winner The winner for which the claim failed + /// @param prizeIndex The prize index for which the claim failed + /// @param reason The revert reason + event ClaimError( + PrizeVault indexed vault, + uint8 indexed tier, + address indexed winner, + uint32 prizeIndex, + bytes reason + ); + + /// @notice Emitted during prize compounding when the account does not have a swapper set + /// @param account The account that does not have a swapper + event SwapperNotSetForWinner(address indexed account); + + /// @notice Thrown when the min token profit is not met for a flash swap or claim batch + /// @param actualProfit The actual profit that would have been received + /// @param minProfit The min profit required + error MinRewardNotMet(uint256 actualProfit, uint256 minProfit); + + /// @notice Prevents reentrancy to any function with this modifier + modifier nonReentrant() { + assembly { + if tload(REENTRANT_STORAGE_KEY) { revert(0, 0) } + tstore(REENTRANT_STORAGE_KEY, 1) + } + _; + assembly { + tstore(REENTRANT_STORAGE_KEY, 0) + } + } + + /// @notice Constructor + /// @param uniV3Router_ The Uniswap V3 router that will be used for compound swaps + /// @param uniV3Oracle_ The UniV3Oracle that will be used to find underlying asset pricing + /// @param swapperFactory_ The 0xSplits swapper factory + /// @param compoundVault_ The prize vault to compound prizes into + /// @param scaledOfferFactor_ Defines the discount (or premium) of swapper offers + constructor( + IUniswapV3Router uniV3Router_, + IUniV3Oracle uniV3Oracle_, + ISwapperFactory swapperFactory_, + PrizeVault compoundVault_, + uint32 scaledOfferFactor_ + ) PrizeCompoundingSwapperHook(uniV3Oracle_, swapperFactory_, compoundVault_, scaledOfferFactor_) { + uniV3Router = uniV3Router_; + } + + /// @notice Claims prizes for winners and auto-compounds if possible. + /// @param tier The prize tier to claim + /// @param winners The winners to claim prizes for + /// @param prizeIndices The prize indices to claim for each winner + /// @param rewardRecipient Where to send the prize token claim rewards + /// @param minReward The min reward required for the total claim batch + /// @return totalReward The total claim rewards collected for the batch + function claimPrizes( + uint8 tier, + address[] calldata winners, + uint32[][] calldata prizeIndices, + address rewardRecipient, + uint256 minReward + ) external nonReentrant returns (uint256 totalReward) { + uint256 prizeSize = prizePool.getTierPrizeSize(tier); + uint256 elapsedTime = block.timestamp - prizePool.drawClosesAt(prizePool.getLastAwardedDrawId()); + uint256 drawPeriod = prizePool.drawPeriodSeconds(); + uint256 normalClaimPeriod = (drawPeriod * CLAIM_FALLBACK_TIME_THRESHOLD) / FACTOR_DENOMINATOR; + if (prizePool.isCanaryTier(tier)) { + // Canary claims + totalReward = _claimPrizes(tier, winners, prizeIndices, uint96(prizeSize)); + prizePool.withdrawRewards(rewardRecipient, totalReward); + } else if (elapsedTime < normalClaimPeriod) { + // Normal claims + _claimPrizes(tier, winners, prizeIndices, 0); + totalReward = _compoundAccounts(winners, rewardRecipient); + } else { + // Fallback claims (no compounding, fee ramps up to max) + uint32 currentRewardFactor = MAX_CLAIM_REWARD_FACTOR; + uint32 minRewardFactor = scaledOfferFactor; + if (minRewardFactor < currentRewardFactor) { + currentRewardFactor = uint32( + minRewardFactor + ( + (MAX_CLAIM_REWARD_FACTOR - minRewardFactor) * + (elapsedTime - normalClaimPeriod) + ) / (drawPeriod - normalClaimPeriod) + ); + } + totalReward = _claimPrizes( + tier, + winners, + prizeIndices, + uint96((prizeSize * currentRewardFactor) / FACTOR_DENOMINATOR) + ); + prizePool.withdrawRewards(rewardRecipient, totalReward); + } + if (totalReward < minReward) { + revert MinRewardNotMet(totalReward, minReward); + } + } + + /// @notice Compounds prizes for the given accounts + /// @param accounts The account addresses to compound (the depositors) + /// @param rewardRecipient The recipient of the flash swap rewards + /// @param minReward The minimum reward required to not revert + /// @return totalReward The total reward received + function compoundAccounts( + address[] calldata accounts, + address rewardRecipient, + uint256 minReward + ) external returns (uint256 totalReward) { + totalReward = _compoundAccounts(accounts, rewardRecipient); + if (totalReward < minReward) { + revert MinRewardNotMet(totalReward, minReward); + } + } + + /// @inheritdoc ISwapperFlashCallback + function swapperFlashCallback( + address tokenToBeneficiary, + uint256 amountToBeneficiary, + bytes calldata /*data*/ + ) external { + address tokenOut = PrizeVault(tokenToBeneficiary).asset(); + address tokenIn = address(prizePool.prizeToken()); + uniV3Router.exactOutputSingle( + IUniswapV3Router.ExactOutputSingleParams({ + tokenIn: tokenIn, + tokenOut: tokenOut, + fee: _uniV3PoolFee(tokenIn, tokenOut), + recipient: address(this), + amountOut: amountToBeneficiary, + amountInMaximum: type(uint256).max, + sqrtPriceLimitX96: 0 + }) + ); + IERC20(tokenOut).forceApprove(tokenToBeneficiary, amountToBeneficiary); + PrizeVault(tokenToBeneficiary).deposit(amountToBeneficiary, address(this)); + IERC20(tokenToBeneficiary).forceApprove(msg.sender, amountToBeneficiary); + } + + /// @notice Claims prizes for a batch of winners and prize indices + function _claimPrizes( + uint8 tier, + address[] calldata winners, + uint32[][] calldata prizeIndices, + uint96 rewardPerClaim + ) internal returns (uint256 totalReward) { + for (uint256 w = 0; w < winners.length; w++) { + for (uint256 p = 0; p < prizeIndices[w].length; p++) { + try + compoundVault.claimPrize(winners[w], tier, prizeIndices[w][p], rewardPerClaim, address(this)) + returns (uint256 /* prizeSize */) { + totalReward += rewardPerClaim; + } catch (bytes memory reason) { + emit ClaimError(compoundVault, tier, winners[w], prizeIndices[w][p], reason); + } + } + } + } + + /// @notice Compounds prizes for the given accounts + function _compoundAccounts(address[] calldata accounts, address rewardRecipient) internal returns (uint256 totalReward) { + // Build quote params + address tokenIn = address(prizePool.prizeToken()); + QuoteParams[] memory quoteParams = new QuoteParams[](1); + quoteParams[0] = QuoteParams({ + quotePair: QuotePair({ + base: tokenIn, + quote: address(compoundVault) + }), + baseAmount: 0, + data: "" + }); + + // Infinite approve the router to spend `tokenIn` + IERC20(tokenIn).forceApprove(address(uniV3Router), type(uint256).max); + + // Flash swap for each account + for (uint256 i = 0; i < accounts.length; i++) { + address swapper = swappers[accounts[i]]; + if (swapper == address(0)) { + emit SwapperNotSetForWinner(accounts[i]); + } else { + quoteParams[0].baseAmount = uint128(IERC20(tokenIn).balanceOf(swapper)); + ISwapper(swapper).flash(quoteParams, ""); + } + } + + // Compute rewards + totalReward = IERC20(tokenIn).balanceOf(address(this)); + if (totalReward > 0) { + IERC20(tokenIn).safeTransfer(rewardRecipient, totalReward); + } + } + + /// @notice Fetches the Uniswap V3 pool fee from the pool that the oracle uses + function _fetchUniV3PoolFee(address base, address quote) internal view returns (uint24) { + QuotePair[] memory quotePairs = new QuotePair[](1); + quotePairs[0] = QuotePair({ + base: base, + quote: quote + }); + return IUniswapV3PoolImmutables( + IUniV3Oracle(address(baseOracle)).getPairDetails(quotePairs)[0].pool + ).fee(); + } + + /// @notice Returns the Uniswap V3 pool fee from teh pool that the oracle uses + /// @dev Caches the result in transient storage for cheap, repetitive access + function _uniV3PoolFee(address base, address quote) internal returns (uint24) { + uint256 fee; + assembly { + fee := tload(UNIV3FEE_STORAGE_KEY) + } + if (fee == 0) { + fee = _fetchUniV3PoolFee(base, quote); + assembly { + tstore(UNIV3FEE_STORAGE_KEY, fee) + } + } + return uint24(fee); + } +} \ No newline at end of file diff --git a/src/prize-hooks/examples/prize-compounding-swapper/interfaces/IUniV3Oracle.sol b/src/prize-hooks/examples/prize-compounding-swapper/interfaces/IUniV3Oracle.sol new file mode 100644 index 0000000..4dad51c --- /dev/null +++ b/src/prize-hooks/examples/prize-compounding-swapper/interfaces/IUniV3Oracle.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.17; + +import { IOracle, QuotePair } from "./IOracle.sol"; + +/// @title Uniswap V3 Oracle Interface +/// @author 0xSplits +interface IUniV3Oracle is IOracle { + + struct InitParams { + address owner; + bool paused; + uint32 defaultPeriod; + SetPairDetailParams[] pairDetails; + } + + struct SetPairDetailParams { + QuotePair quotePair; + PairDetail pairDetail; + } + + struct PairDetail { + address pool; + uint32 period; + } + + function getPairDetails(QuotePair[] calldata quotePairs) external view returns (PairDetail[] memory pairDetails); +} \ No newline at end of file diff --git a/src/prize-hooks/examples/prize-compounding-swapper/interfaces/IUniV3OracleFactory.sol b/src/prize-hooks/examples/prize-compounding-swapper/interfaces/IUniV3OracleFactory.sol new file mode 100644 index 0000000..79a749c --- /dev/null +++ b/src/prize-hooks/examples/prize-compounding-swapper/interfaces/IUniV3OracleFactory.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.17; + +import { IUniV3Oracle } from "./IUniV3Oracle.sol"; + +/// @title Uniswap V3 Oracle Factory Interface +/// @author 0xSplits +interface IUniV3OracleFactory { + function createUniV3Oracle(IUniV3Oracle.InitParams calldata params) external returns (IUniV3Oracle); +} \ No newline at end of file diff --git a/src/prize-hooks/examples/prize-compounding-swapper/interfaces/IUniswapV3PoolImmutables.sol b/src/prize-hooks/examples/prize-compounding-swapper/interfaces/IUniswapV3PoolImmutables.sol new file mode 100644 index 0000000..13a0dff --- /dev/null +++ b/src/prize-hooks/examples/prize-compounding-swapper/interfaces/IUniswapV3PoolImmutables.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +/// @title Pool state that never changes +/// @notice These parameters are fixed for a pool forever, i.e., the methods will always return the same values +interface IUniswapV3PoolImmutables { + /// @notice The contract that deployed the pool, which must adhere to the IUniswapV3Factory interface + /// @return The contract address + function factory() external view returns (address); + + /// @notice The first of the two tokens of the pool, sorted by address + /// @return The token contract address + function token0() external view returns (address); + + /// @notice The second of the two tokens of the pool, sorted by address + /// @return The token contract address + function token1() external view returns (address); + + /// @notice The pool's fee in hundredths of a bip, i.e. 1e-6 + /// @return The fee + function fee() external view returns (uint24); + + /// @notice The pool tick spacing + /// @dev Ticks can only be used at multiples of this value, minimum of 1 and always positive + /// e.g.: a tickSpacing of 3 means ticks can be initialized every 3rd tick, i.e., ..., -6, -3, 0, 3, 6, ... + /// This value is an int24 to avoid casting even though it is always positive. + /// @return The tick spacing + function tickSpacing() external view returns (int24); + + /// @notice The maximum amount of position liquidity that can use any tick in the range + /// @dev This parameter is enforced per tick to prevent liquidity from overflowing a uint128 at any point, and + /// also prevents out-of-range liquidity from being used to prevent adding in-range liquidity to a pool + /// @return The max amount of liquidity per tick + function maxLiquidityPerTick() external view returns (uint128); +} \ No newline at end of file diff --git a/src/prize-hooks/examples/prize-compounding-swapper/interfaces/IUniswapV3Router.sol b/src/prize-hooks/examples/prize-compounding-swapper/interfaces/IUniswapV3Router.sol new file mode 100644 index 0000000..d811086 --- /dev/null +++ b/src/prize-hooks/examples/prize-compounding-swapper/interfaces/IUniswapV3Router.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +interface IUniswapV3Router { + + struct ExactOutputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 amountOut; + uint256 amountInMaximum; + uint160 sqrtPriceLimitX96; + } + + function exactOutputSingle(ExactOutputSingleParams calldata params) + external + payable + returns (uint256 amountIn); +} \ No newline at end of file diff --git a/test/prize-hooks/examples/prize-compounding-swapper/PrizeCompoundingSwapperHook.t.sol b/test/prize-hooks/examples/prize-compounding-swapper/PrizeCompoundingSwapperHook.t.sol deleted file mode 100644 index bf82d09..0000000 --- a/test/prize-hooks/examples/prize-compounding-swapper/PrizeCompoundingSwapperHook.t.sol +++ /dev/null @@ -1,208 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import { Test } from "forge-std/Test.sol"; - -import { PrizeCompoundingSwapperHook, IERC4626, IOracle, ISwapperFactory, ISwapper } from "src/prize-hooks/examples/prize-compounding-swapper/PrizeCompoundingSwapperHook.sol"; -import { GenericSwapperHelper, FlashInfo, IERC20, QuoteParams, QuotePair } from "src/prize-hooks/examples/prize-compounding-swapper/GenericSwapperHelper.sol"; -import { PrizeHooks } from "pt-v5-vault/interfaces/IPrizeHooks.sol"; - -contract PrizeCompoundingSwapperHookTest is Test { - - uint256 fork; - uint256 forkBlock = 19424769; - uint256 forkTimestamp = 1725638885; - - PrizeCompoundingSwapperHook public prizeCompoundingSwapperHook; - GenericSwapperHelper public swapperHelper; - - IERC4626 public przWETH = IERC4626(address(0x4E42f783db2D0C5bDFf40fDc66FCAe8b1Cda4a43)); - IERC4626 public przPOOL = IERC4626(address(0x6B5a5c55E9dD4bb502Ce25bBfbaA49b69cf7E4dd)); - IERC4626 public przUSDC = IERC4626(address(0x7f5C2b379b88499aC2B997Db583f8079503f25b9)); - - address public WETH = address(0x4200000000000000000000000000000000000006); - address public POOL = address(0xd652C5425aea2Afd5fb142e120FeCf79e18fafc3); - address public wethWhale = address(0xcDAC0d6c6C59727a65F871236188350531885C43); - IOracle public baseOracle = IOracle(address(0x6B99b2E868B6E3B8b259E296c4c6aBffbB1AaB94)); - ISwapperFactory public swapperFactory = ISwapperFactory(address(0xa244bbe019cf1BA177EE5A532250be2663Fb55cA)); - uint32 public defaultScaleFactor = uint32(99e4); // 99% (1% discount to swappers) - address public beefyTokenManager = address(0x3fBD1da78369864c67d62c242d30983d6900c0f0); - address public beefyZapRouter = address(0x6F19Da51d488926C007B9eBaa5968291a2eC6a63); - - address public alice; - - function setUp() public { - fork = vm.createFork('base', forkBlock); - vm.selectFork(fork); - vm.warp(forkTimestamp); - alice = makeAddr("Alice"); - prizeCompoundingSwapperHook = new PrizeCompoundingSwapperHook( - baseOracle, - swapperFactory, - defaultScaleFactor - ); - swapperHelper = new GenericSwapperHelper(); - } - - function testQuoteWethToPrzWeth() external { - QuoteParams[] memory quoteParams = new QuoteParams[](1); - quoteParams[0] = QuoteParams({ - quotePair: QuotePair({ - base: WETH, - quote: address(przWETH) - }), - baseAmount: 1e18, - data: "" - }); - uint256[] memory amounts = prizeCompoundingSwapperHook.getQuoteAmounts(quoteParams); - assertEq(amounts[0], 1e18); - } - - function testQuotePoolToPrzPool() external { - QuoteParams[] memory quoteParams = new QuoteParams[](1); - quoteParams[0] = QuoteParams({ - quotePair: QuotePair({ - base: POOL, - quote: address(przPOOL) - }), - baseAmount: 1e18, - data: "" - }); - uint256[] memory amounts = prizeCompoundingSwapperHook.getQuoteAmounts(quoteParams); - assertEq(amounts[0], 1e18); - } - - function testQuoteWethToPrzUsdc() external { - QuoteParams[] memory quoteParams = new QuoteParams[](1); - quoteParams[0] = QuoteParams({ - quotePair: QuotePair({ - base: WETH, - quote: address(przUSDC) - }), - baseAmount: 1e18, - data: "" - }); - uint256[] memory amounts = prizeCompoundingSwapperHook.getQuoteAmounts(quoteParams); - assertApproxEqAbs(amounts[0], 2291e6, 10e6); - } - - function testQuoteWethToUsdc() external { - QuoteParams[] memory quoteParams = new QuoteParams[](1); - quoteParams[0] = QuoteParams({ - quotePair: QuotePair({ - base: WETH, - quote: przUSDC.asset() - }), - baseAmount: 1e18, - data: "" - }); - uint256[] memory amounts = prizeCompoundingSwapperHook.getQuoteAmounts(quoteParams); - assertApproxEqAbs(amounts[0], 2291e6, 10e6); - } - - function testSwapWethToPrzWeth() external { - // Set a new swapper on the hook - ISwapper swapper = prizeCompoundingSwapperHook.setSwapper(address(przWETH)); - assertEq(swapper.tokenToBeneficiary(), address(przWETH)); - assertEq(prizeCompoundingSwapperHook.swappers(address(this)), address(swapper)); - - // Simulate a prize being won - uint256 prizeAmount = 1e18; - vm.startPrank(wethWhale); - (address prizeRecipient,) = prizeCompoundingSwapperHook.beforeClaimPrize(address(this), 0, 0, 0, address(0)); - assertEq(prizeRecipient, address(swapper)); - IERC20(WETH).transfer(prizeRecipient, prizeAmount); - vm.stopPrank(); - - // Initiate the flash swap by instructing it to deposit to przWETH - swapperHelper.flashSwap( - swapper, - FlashInfo({ - profitRecipient: alice, - tokenToSwap: WETH, - amountToSwap: uint128(prizeAmount), - minTokenToSwapProfit: 0, - minTokenToBeneficiaryProfit: prizeAmount / 100, // 1% - approveTo: address(0), - callTarget: address(przWETH), - callData: abi.encodeWithSelector(IERC4626.deposit.selector, prizeAmount, address(swapperHelper)) - }) - ); - - assertEq(IERC20(WETH).balanceOf(address(swapperHelper)), 0, "no more WETH in swapper helper"); - assertEq(przWETH.balanceOf(address(swapperHelper)), 0, "no more przWETH in swapper helper"); - - assertEq(IERC20(WETH).balanceOf(address(swapper)), 0, "no more WETH in swapper"); - assertEq(przWETH.balanceOf(address(swapper)), 0, "no more przWETH in swapper"); - - assertEq(IERC20(WETH).balanceOf(address(alice)), 0, "no WETH sent to alice"); - assertEq(przWETH.balanceOf(address(alice)), prizeAmount / 100, "1% of przWETH sent to alice"); - - assertEq(IERC20(WETH).balanceOf(address(this)), 0, "no WETH sent to winner"); - assertEq(przWETH.balanceOf(address(this)), prizeAmount - (prizeAmount / 100), "rest of prize amount sent to winner"); - } - - function testSwapWethToPrzUsdc() external { - // Set a new swapper on the hook - ISwapper swapper = prizeCompoundingSwapperHook.setSwapper(address(przUSDC)); - assertEq(swapper.tokenToBeneficiary(), address(przUSDC)); - assertEq(prizeCompoundingSwapperHook.swappers(address(this)), address(swapper)); - - // Simulate a prize being won - uint256 prizeAmount = 8e14; - vm.startPrank(wethWhale); - (address prizeRecipient,) = prizeCompoundingSwapperHook.beforeClaimPrize(address(this), 0, 0, 0, address(0)); - assertEq(prizeRecipient, address(swapper)); - IERC20(WETH).transfer(prizeRecipient, prizeAmount); - vm.stopPrank(); - - // Use pre-simulated beefy swap router swap in the flash info - swapperHelper.flashSwap( - swapper, - FlashInfo({ - profitRecipient: alice, - tokenToSwap: WETH, - amountToSwap: uint128(prizeAmount), - minTokenToSwapProfit: 0, - minTokenToBeneficiaryProfit: 0.01e6, // 1% of $1 worth of ETH should be over $0.01 of USDC - approveTo: beefyTokenManager, - callTarget: beefyZapRouter, - callData: abi.encodePacked( - hex"f41b2db6000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000002c000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000", - address(swapperHelper), - hex"000000000000000000000000", - address(swapperHelper), - hex"000000000000000000000000000000000000000000000000000000000000000100000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000002d79883d200000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000007f5c2b379b88499ac2b997db583f8079503f25b900000000000000000000000000000000000000000000000000000000001bc5ae000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000006a00000000000000000000000006a000f20005980200259b80c51020030400010680000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000544e3ead59e0000000000000000000000003800091020a00290f20606b000000000e38c33ef0000000000000000000000004200000000000000000000000000000000000006000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000002d79883d2000000000000000000000000000000000000000000000000000000000000001bc5af00000000000000000000000000000000000000000000000000000000001c0d7f6be459e5f88448e28832993c215d8fc40000000000000000000000000128668700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000636162616e6110000000000000000000000000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000003a000000000000000000000000000000160000000000000010c0000000000000e101e891c9f96dca29da8b97be3403d16135ebe80280000014000040000ff00000300000000000000000000000000000000000000000000000000000000f41766d8000000000000000000000000000000000000000000000000000105ef39b20000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000006a000f20005980200259b80c51020030400010680000000000000000000000000000000000000000000000000000000066db2a6f00000000000000000000000000000000000000000000000000000000000000010000000000000000000000004200000000000000000000000000000000000006000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000190042000000000000000000000000000000000000060000006000240000ff00000300000000000000000000000000000000000000000000000000000000a9059cbb000000000000000000000000994c90f2e654b282e24a1b7d00ee12e82408312c0000000000000000000000000000000000000000000000000001d1a94a200000994c90f2e654b282e24a1b7d00ee12e82408312c000001200024000020000003000000000000000000000000000000000000000000000000000000003eece7db0000000000000000000000006a000f20005980200259b80c51020030400010680000000000000000000000000000000000000000000000000001d1a94a20000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc500000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000004200000000000000000000000000000000000006ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000007f5c2b379b88499ac2b997db583f8079503f25b900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000446e553f6500000000000000000000000000000000000000000000000000000000000000000000000000000000000000006f19da51d488926c007b9ebaa5968291a2ec6a63000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000000000004" - ) - }) - ); - - assertEq(IERC20(WETH).balanceOf(address(swapperHelper)), 0, "no more WETH in swapper helper"); - assertEq(przUSDC.balanceOf(address(swapperHelper)), 0, "no more przUSDC in swapper helper"); - - assertEq(IERC20(WETH).balanceOf(address(swapper)), 0, "no more WETH in swapper"); - assertEq(przUSDC.balanceOf(address(swapper)), 0, "no more przUSDC in swapper"); - - assertEq(IERC20(WETH).balanceOf(address(alice)), 0, "no WETH sent to alice"); - assertGe(przUSDC.balanceOf(address(alice)), 0.01e6, "1% of przUSDC sent to alice"); - - assertEq(IERC20(WETH).balanceOf(address(this)), 0, "no WETH sent to winner"); - assertGe(przUSDC.balanceOf(address(this)), 1.7e6, "rest of prize amount sent to winner"); - } - - function testOverrideSwapper() external { - // Set a new swapper on the hook - ISwapper swapper1 = prizeCompoundingSwapperHook.setSwapper(address(przUSDC)); - assertEq(prizeCompoundingSwapperHook.swappers(address(this)), address(swapper1)); - assertEq(swapper1.owner(), address(prizeCompoundingSwapperHook)); - - // Override the swapper and claim ownership of the old one - ISwapper swapper2 = prizeCompoundingSwapperHook.setSwapper(address(przWETH)); - assertEq(prizeCompoundingSwapperHook.swappers(address(this)), address(swapper2)); - assertNotEq(address(swapper1), address(swapper2)); - - assertEq(swapper1.owner(), address(this)); - assertEq(swapper2.owner(), address(prizeCompoundingSwapperHook)); - } - -} diff --git a/test/prize-hooks/examples/prize-compounding-swapper/PrizeCompoundingSwapperHookAndClaimer.t.sol b/test/prize-hooks/examples/prize-compounding-swapper/PrizeCompoundingSwapperHookAndClaimer.t.sol new file mode 100644 index 0000000..31b397d --- /dev/null +++ b/test/prize-hooks/examples/prize-compounding-swapper/PrizeCompoundingSwapperHookAndClaimer.t.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import { Test } from "forge-std/Test.sol"; + +import { + PrizeCompoundingSwapperHookAndClaimer, + PrizeVault, + IUniV3Oracle, + ISwapperFactory, + ISwapper, + IUniswapV3Router, + IERC20, + QuoteParams, + QuotePair +} from "src/prize-hooks/examples/prize-compounding-swapper/PrizeCompoundingSwapperHookAndClaimer.sol"; +import { PrizeHooks } from "pt-v5-vault/interfaces/IPrizeHooks.sol"; + +contract PrizeCompoundingSwapperHookAndClaimerTest is Test { + + uint256 fork; + uint256 forkBlock = 19424769; + uint256 forkTimestamp = 1725638885; + + PrizeCompoundingSwapperHookAndClaimer public hookAndClaimer; + + PrizeVault public przWETH = PrizeVault(address(0x4E42f783db2D0C5bDFf40fDc66FCAe8b1Cda4a43)); + PrizeVault public przPOOL = PrizeVault(address(0x6B5a5c55E9dD4bb502Ce25bBfbaA49b69cf7E4dd)); + PrizeVault public przUSDC = PrizeVault(address(0x7f5C2b379b88499aC2B997Db583f8079503f25b9)); + + address public WETH = address(0x4200000000000000000000000000000000000006); + address public POOL = address(0xd652C5425aea2Afd5fb142e120FeCf79e18fafc3); + address public wethWhale = address(0xcDAC0d6c6C59727a65F871236188350531885C43); + IUniV3Oracle public baseOracle = IUniV3Oracle(address(0x6B99b2E868B6E3B8b259E296c4c6aBffbB1AaB94)); + ISwapperFactory public swapperFactory = ISwapperFactory(address(0xa244bbe019cf1BA177EE5A532250be2663Fb55cA)); + IUniswapV3Router public uniV3Router = IUniswapV3Router(address(0x2626664c2603336E57B271c5C0b26F421741e481)); + uint32 public defaultScaleFactor = uint32(99e4); // 99% (1% discount to swappers) + + address public alice; + + function setUp() public { + fork = vm.createFork('base', forkBlock); + vm.selectFork(fork); + vm.warp(forkTimestamp); + alice = makeAddr("Alice"); + hookAndClaimer = new PrizeCompoundingSwapperHookAndClaimer( + uniV3Router, + baseOracle, + swapperFactory, + przUSDC, + defaultScaleFactor + ); + } + + function testQuoteWethToPrzWeth() external { + QuoteParams[] memory quoteParams = new QuoteParams[](1); + quoteParams[0] = QuoteParams({ + quotePair: QuotePair({ + base: WETH, + quote: address(przWETH) + }), + baseAmount: 1e18, + data: "" + }); + uint256[] memory amounts = hookAndClaimer.getQuoteAmounts(quoteParams); + assertEq(amounts[0], 1e18); + } + + function testQuotePoolToPrzPool() external { + QuoteParams[] memory quoteParams = new QuoteParams[](1); + quoteParams[0] = QuoteParams({ + quotePair: QuotePair({ + base: POOL, + quote: address(przPOOL) + }), + baseAmount: 1e18, + data: "" + }); + uint256[] memory amounts = hookAndClaimer.getQuoteAmounts(quoteParams); + assertEq(amounts[0], 1e18); + } + + function testQuoteWethToPrzUsdc() external { + QuoteParams[] memory quoteParams = new QuoteParams[](1); + quoteParams[0] = QuoteParams({ + quotePair: QuotePair({ + base: WETH, + quote: address(przUSDC) + }), + baseAmount: 1e18, + data: "" + }); + uint256[] memory amounts = hookAndClaimer.getQuoteAmounts(quoteParams); + assertApproxEqAbs(amounts[0], 2291e6, 10e6); + } + + function testQuoteWethToUsdc() external { + QuoteParams[] memory quoteParams = new QuoteParams[](1); + quoteParams[0] = QuoteParams({ + quotePair: QuotePair({ + base: WETH, + quote: przUSDC.asset() + }), + baseAmount: 1e18, + data: "" + }); + uint256[] memory amounts = hookAndClaimer.getQuoteAmounts(quoteParams); + assertApproxEqAbs(amounts[0], 2291e6, 10e6); + } + +}