diff --git a/.env.example b/.env.example index 54da2a0..acd0a09 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,4 @@ -export API_KEY_ALCHEMY="YOUR_API_KEY_ALCHEMY" -export API_KEY_ETHERSCAN="YOUR_API_KEY_ETHERSCAN" -export API_KEY_POLYGONSCAN="YOUR_API_KEY_POLYGONSCAN" -export MNEMONIC="YOUR_MNEMONIC" -export FOUNDRY_PROFILE="default" +PRIVATE_KEY="PRIVATE_KEY" +ALCHEMY_API_KEY="ALCHEMY_API_KEY" +# Unified Etherscan V2 API key used by `rocketh-verify etherscan` +ETHERSCAN_API_KEY="ETHERSCAN_API_KEY" diff --git a/.gitignore b/.gitignore index 594fdf4..9cf4bc2 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ lcov.info artifacts cache_hardhat deployments +generated diff --git a/.prettierignore b/.prettierignore index d85a8b5..daf5db6 100644 --- a/.prettierignore +++ b/.prettierignore @@ -17,3 +17,4 @@ package-lock.json pnpm-lock.yaml yarn.lock deployments +.claude diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..c681802 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,101 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for +everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity +and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, +or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take +appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, +issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for +moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing +the community in public spaces. Examples of representing our community include using an official e-mail address, posting +via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible +for enforcement at contact@cooperlabs.xyz. All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem +in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the +community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation +and an explanation of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including +unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding +interactions in community spaces as well as external channels like social media. Violating these terms may lead to a +temporary or permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified +period of time. No public or private interaction with the people involved, including unsolicited interaction with those +enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate +behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. +Translations are available at https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..760dfd7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,68 @@ +# Contribution Guidelines + +Thanks for your interest in contributing to the Transmuter system! Transmuter is a community built protocol and anyone +is welcome to improve it. + +If you need to get in contact with the repository maintainers, please reach out in the +[#community-dev channel](https://discord.gg/DgFR5T4DbM) of Parallel Discord Server. + +## Types of Contributing + +There are many ways to contribute, but here are a few if you want a place to start: + +1. **Opening an issue.** Before opening an issue, please check that there is not an issue already open. If there is, + feel free to comment more details, explanations, or examples within the open issue rather than duplicating it. + Suggesting changes to the open development process are within the bounds of opening issues. We are always open to + feedback and receptive to suggestions! +2. **Resolving an issue.** You can resolve an issue either by showing that it is not an issue or by fixing the issue + with code changes, additional tests, etc. Any pull request fixing an issue should reference that issue. +3. **Reviewing open PRs.** You can provide comments, standards guidance, naming suggestions, gas optimizations, or ideas + for alternative designs on any open pull request. + +## Opening an Issue + +When opening an [issue](https://github.com/parallel-protocol/parallel-parallelizer/issues), please title it with a +concise problem statement and check that a similar request is not already open or in progress. Not all issues may be +deemed worth resolving, so please follow through with responding to any questions or comments that others may have +regarding the issue. + +## Opening a Pull Request + +All pull requests should be opened against the `main` branch. In the pull request, please reference the issue you are +fixing. + +Pull requests can be reviewed by community members, but to be merged they will need approval from the repository +maintainers. Please understand it will take time to receive a response, although the maintainers will aim to respond and +comment as soon as possible. + +**For larger, more substantial changes to the code, it is best to open an issue and start a discussion with the +maintainers to align on the change before spending time on the development.** + +Finally, before opening a pull request please do the following: + +- Run the Foundry tests with `bun run test` +- Document any new functions, structs, or interfaces following the natspec standard. +- Add tests! For smaller contributions, they should be tested with unit tests, and fuzz tests where possible. For bigger + contributions, they should be tested with invariant tests where possible. + +## Standards + +All contributions must follow the below standards. Maintainers will close out PRs that do not adhere to these standards. + +1. All contracts should be formatted with the prettier used in the default vscode config. +2. These contracts follow the [solidity style guide](https://docs.soliditylang.org/en/v0.8.19/style-guide.html) with one + minor exception of using the `\_prependUnderscore` style naming for internal contract functions, internal top-level + parameters, and function parameters with naming collisions. +3. All external facing contracts should inherit from interfaces, which specify and document its functions with natspec. +4. Picking up stale issues by other authors is fine! Please just communicate with them ahead of time and it is best + practice to include co-authors in any commits. +5. Squash commits where possible to make reviews clean and efficient. PRs that are merged to main will be squashed into + 1 commit. + +## Code of Conduct + +Above all else, please be respectful of the people behind the code. Any kind of aggressive or disrespectful comments, +issues, and language will be removed. + +Issues and PRs that are obviously spam and unhelpful to the development process or unrelated to the core code will also +be closed. diff --git a/README.md b/README.md index 7f56088..ad2a2dd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Parallel Tokenomics -## 1. Overview +## Summary The Parallel Tokenomics system consists of smart contracts that enable: @@ -29,22 +29,11 @@ The Parallel Tokenomics system consists of smart contracts that enable: - MainFeeDistributor: Mainnet - SideChainFeeDistributor: Polygon, Fantom -The high-level architecture of the protocol is shown below: +## Architecture ![High Level Architecture](./docs/assets/high-level-architecture.png) -## 2. Folder Structure - -- [Broadcast](./broadcast) folder contains Foundry transactions executed by scripts. -- [Contracts](./contracts) folder contains contracts source code. -- [Deploy](./deploy) folder contains hardhat deployment scripts. -- [Deployments](./deployments) folder contains info of contracts deployed per network. -- [Docs](./docs) folder contains all documentation related to main contracts. -- [Script](./scripts) folder contains Foundry scripts to interact with onchain contracts. -- [Test](./test) folder contains all tests related to the contracts with mocks and settings. -- [Utils](./utils) folder contains helper functions. - -## 3. Documentation +## Documentation Links Additional documentation can be found in the `/docs` directory: @@ -52,7 +41,7 @@ Additional documentation can be found in the `/docs` directory: - [Deployments Contracts](docs/Deployment.md) - [Technical Specifications](docs/TechnicalSpecs.md) -## 4. Getting Started +## Security ### Foundry @@ -65,18 +54,14 @@ Foundry is used for testing and scripting. To bun i ``` -### Fill the `.env` file with your data - -The Foundry script relies solely on the PRIVATE_KEY. The MNEMONIC is used on the Hardhat side and will override the -PRIVATE_KEY if it is defined. +### Setup `.env` file ```bash -export API_KEY_ALCHEMY="YOUR_API_KEY_ALCHEMY" -export API_KEY_ETHERSCAN="YOUR_API_KEY_ETHERSCAN" -export API_KEY_POLYGONSCAN="YOUR_API_KEY_POLYGONSCAN" -export MNEMONIC="YOUR_MNEMONIC" -export PRIVATE_KEY="YOUR_PRIVATE_KEY" -export FOUNDRY_PROFILE="default" +MNEMONIC="YOUR_MNEMONIC" +PRIVATE_KEY="PRIVATE_KEY" +ALCHEMY_API_KEY="ALCHEMY_API_KEY" +MAINNET_ETHERSCAN_API_KEY="MAINNET_ETHERSCAN_API_KEY" +POLYGON_ETHERSCAN_API_KEY="POLYGON_ETHERSCAN_API_KEY" ``` ### Compile contracts @@ -93,6 +78,20 @@ bun run test You will find other useful commands in the [package.json](./package.json) file. -## Licences +## Contributing + +If you're interested in contributing, please see our [contributions guidelines](./CONTRIBUTING.md). + +## Questions & Feedback + +For any question or feedback you can use [discord](https://discord.com/invite/mimodao). Don't hesitate to reach out on +[Twitter](https://twitter.com/mimo_labs)🐦 as well. + +## Licensing + +The primary license for this repository is the MIT license. See [`LICENSE`](./LICENSE). Minus the following exceptions: + +- tests files are under UNLICENSED license +- mocks contracts are under UNLICENSED license -All contracts is under the `MIT` License, see [`LICENSE`](./LICENSE). +Each of these files states their license type. diff --git a/bun.lockb b/bun.lockb index d047fc1..96e8f35 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/commitlint.config.js b/commitlint.config.cjs similarity index 100% rename from commitlint.config.js rename to commitlint.config.cjs diff --git a/contracts/sPRL/TimeLockPenaltyERC20.sol b/contracts/sPRL/TimeLockPenaltyERC20.sol index 8df287e..7648368 100644 --- a/contracts/sPRL/TimeLockPenaltyERC20.sol +++ b/contracts/sPRL/TimeLockPenaltyERC20.sol @@ -7,6 +7,7 @@ import { ERC20Votes } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20 import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { AccessManaged } from "@openzeppelin/contracts/access/manager/AccessManaged.sol"; import { Pausable } from "@openzeppelin/contracts/utils/Pausable.sol"; import { Nonces } from "@openzeppelin/contracts/utils/Nonces.sol"; @@ -22,6 +23,7 @@ import { MathsLib } from "contracts/libraries/MathsLib.sol"; /// @custom:contact security@cooperlabs.xyz abstract contract TimeLockPenaltyERC20 is ERC20, ERC20Permit, ERC20Votes, AccessManaged, Pausable, ReentrancyGuard { using MathsLib for *; + using SafeERC20 for IERC20; //------------------------------------------- // Storage @@ -115,6 +117,11 @@ abstract contract TimeLockPenaltyERC20 is ERC20, ERC20Permit, ERC20Votes, Access /// @param newPercentage The new penalty percentage. event StartPenaltyPercentageUpdated(uint256 oldPercentage, uint256 newPercentage); + /// @notice Emitted when extra tokens are recovered to the fee receiver. + /// @param token The address of the recovered token. + /// @param amount The amount of tokens recovered. + event TokensRecovered(address token, uint256 amount); + //------------------------------------------- // Errors //------------------------------------------- @@ -129,6 +136,12 @@ abstract contract TimeLockPenaltyERC20 is ERC20, ERC20Permit, ERC20Votes, Access error PercentageOutOfRange(uint256 attemptedPercentage); /// @notice Thrown when the penalty percentage is too high. error MaxPenaltyPercentageExceeded(); + /// @notice Thrown when a withdrawal request is made with a zero amount. + error NullAmount(); + /// @notice Thrown when the fee receiver is set to the zero address. + error FeeReceiverZeroAddress(); + /// @notice Thrown when trying to recover the underlying token, which backs user shares. + error CannotRecoverUnderlying(); //------------------------------------------- // Constructor @@ -158,6 +171,9 @@ abstract contract TimeLockPenaltyERC20 is ERC20, ERC20Permit, ERC20Votes, Access if (_startPenaltyPercentage > MAX_PENALTY_PERCENTAGE) { revert PercentageOutOfRange(_startPenaltyPercentage); } + if (_feeReceiver == address(0)) { + revert FeeReceiverZeroAddress(); + } feeReceiver = _feeReceiver; underlying = IERC20(_underlying); @@ -172,6 +188,7 @@ abstract contract TimeLockPenaltyERC20 is ERC20, ERC20Permit, ERC20Votes, Access /// @notice Request to withdraw assets from the contract. /// @param _unlockingAmount The amount of assets to unlock. function requestWithdraw(uint256 _unlockingAmount) external { + if (_unlockingAmount == 0) revert NullAmount(); _burn(msg.sender, _unlockingAmount); uint256 id = userVsNextID[msg.sender]++; @@ -266,6 +283,9 @@ abstract contract TimeLockPenaltyERC20 is ERC20, ERC20Permit, ERC20Votes, Access /// @notice Allow the AccessManager to update the fee receiver address. /// @param _newFeeReceiver The new fee receiver. function updateFeeReceiver(address _newFeeReceiver) public virtual restricted { + if (_newFeeReceiver == address(0)) { + revert FeeReceiverZeroAddress(); + } emit FeeReceiverUpdated(_newFeeReceiver); feeReceiver = _newFeeReceiver; } @@ -282,6 +302,17 @@ abstract contract TimeLockPenaltyERC20 is ERC20, ERC20Permit, ERC20Votes, Access _unpause(); } + /// @notice Recover extra tokens sitting idle in the contract (e.g. reward tokens sent on its behalf). + /// @dev The underlying token cannot be recovered as it backs user shares. Recovered tokens are sent to the fee + /// receiver. + /// @param _token The token to recover. + function recoverERC20(IERC20 _token) external restricted { + if (_token == underlying) revert CannotRecoverUnderlying(); + uint256 balance = _token.balanceOf(address(this)); + emit TokensRecovered(address(_token), balance); + _token.safeTransfer(feeReceiver, balance); + } + //------------------------------------------- // Private/Internal functions //------------------------------------------- diff --git a/contracts/sPRL/sPRL1.sol b/contracts/sPRL/sPRL1.sol index 6838e79..b315ca2 100644 --- a/contracts/sPRL/sPRL1.sol +++ b/contracts/sPRL/sPRL1.sol @@ -32,13 +32,7 @@ contract sPRL1 is TimeLockPenaltyERC20 { uint64 _timeLockDuration ) TimeLockPenaltyERC20( - NAME, - SYMBOL, - _underlying, - _feeReceiver, - _accessManager, - _startPenaltyPercentage, - _timeLockDuration + NAME, SYMBOL, _underlying, _feeReceiver, _accessManager, _startPenaltyPercentage, _timeLockDuration ) { } @@ -71,8 +65,9 @@ contract sPRL1 is TimeLockPenaltyERC20 { nonReentrant { // @dev using try catch to avoid reverting the transaction in case of front-running - try IERC20Permit(address(underlying)).permit(msg.sender, address(this), _assetAmount, _deadline, _v, _r, _s) { - } catch { } + try IERC20Permit(address(underlying)) + .permit(msg.sender, address(this), _assetAmount, _deadline, _v, _r, _s) { } + catch { } underlying.safeTransferFrom(msg.sender, address(this), _assetAmount); _deposit(_assetAmount); } diff --git a/contracts/sPRL/sPRL2.sol b/contracts/sPRL/sPRL2.sol index b40f285..dcb19d5 100644 --- a/contracts/sPRL/sPRL2.sol +++ b/contracts/sPRL/sPRL2.sol @@ -24,7 +24,7 @@ contract sPRL2 is TimeLockPenaltyERC20 { string constant SYMBOL = "sPRL2"; /// @dev Aura Pool PID is hardcoded and must be updated before deploying - uint256 public constant AURA_POOL_PID = 244; + uint256 public constant AURA_POOL_PID = 258; //------------------------------------------- // Storage @@ -117,13 +117,7 @@ contract sPRL2 is TimeLockPenaltyERC20 { BPTConfigParams memory _configParams ) TimeLockPenaltyERC20( - NAME, - SYMBOL, - _stakedAuraBPT, - _feeReceiver, - _accessManager, - _startPenaltyPercentage, - _timeLockDuration + NAME, SYMBOL, _stakedAuraBPT, _feeReceiver, _accessManager, _startPenaltyPercentage, _timeLockDuration ) { if (_configParams.rewardTokens.length == 0) { diff --git a/contracts/sPRL/sPRL2Migrator.sol b/contracts/sPRL/sPRL2Migrator.sol new file mode 100644 index 0000000..a45ad90 --- /dev/null +++ b/contracts/sPRL/sPRL2Migrator.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import { sPRL2 } from "./sPRL2.sol"; +import { sPRL2V2 } from "./sPRL2V2.sol"; + +/// @title sPRL2Migrator +/// @author Cooper Labs +/// @custom:contact security@cooperlabs.xyz +/// @notice Migrates sPRL2 positions to sPRL2V2 in a single transaction. +/// @dev Requires sPRL2 to be paused so that emergencyWithdraw is available. +contract sPRL2Migrator { + using SafeERC20 for IERC20; + + sPRL2 public immutable SPRL2; + sPRL2V2 public immutable SPRL2V2; + IERC20 public immutable BPT; + + error MigrationAmountZero(); + + event Migrated(address indexed user, uint256 amount); + + constructor(sPRL2 _sprl2, sPRL2V2 _sprl2v2) { + SPRL2 = _sprl2; + SPRL2V2 = _sprl2v2; + BPT = _sprl2v2.BPT(); + + BPT.approve(address(_sprl2v2), type(uint256).max); + } + + /// @notice Migrate sPRL2 tokens to sPRL2V2. + /// @dev User must approve this contract to spend their sPRL2 tokens. + /// @dev sPRL2 must be paused for emergencyWithdraw to work. + /// @param _amount The amount of sPRL2 to migrate. + function migrate(uint256 _amount) external { + if (_amount == 0) revert MigrationAmountZero(); + + // Transfer sPRL2 from user + IERC20(address(SPRL2)).safeTransferFrom(msg.sender, address(this), _amount); + + // Emergency withdraw BPT from sPRL2 (requires paused) + SPRL2.emergencyWithdraw(_amount); + + // Deposit BPT into sPRL2V2 (mints sPRL2V2 to this contract) + SPRL2V2.depositBPT(_amount); + + // Transfer sPRL2V2 to user + IERC20(address(SPRL2V2)).safeTransfer(msg.sender, _amount); + + emit Migrated(msg.sender, _amount); + } +} diff --git a/contracts/sPRL/sPRL2V2.sol b/contracts/sPRL/sPRL2V2.sol new file mode 100644 index 0000000..3b8eb74 --- /dev/null +++ b/contracts/sPRL/sPRL2V2.sol @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { TimeLockPenaltyERC20, IERC20, IERC20Permit } from "./TimeLockPenaltyERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; + +import { IBalancerV3Router } from "contracts/interfaces/IBalancerV3Router.sol"; +import { IPermit2 } from "contracts/interfaces/IPermit2.sol"; +import { IWrappedNative } from "contracts/interfaces/IWrappedNative.sol"; + +/// @title sPRL2V2 +/// @author Cooper Labs +/// @custom:contact security@cooperlabs.xyz +/// @notice sPRL2V2 is a staking contract that allows users to deposit PRL and WETH into a Balancer V3 pool. +/// The BPT is held by the contract. +contract sPRL2V2 is TimeLockPenaltyERC20 { + using Address for address payable; + using SafeERC20 for IERC20; + + string constant NAME = "Stake 20WETH-80PRL Deposit Vault"; + string constant SYMBOL = "sPRL2V2"; + + //------------------------------------------- + // Storage + //------------------------------------------- + + /// @notice The configuration parameters for the contract. + struct BPTConfigParams { + IBalancerV3Router balancerRouter; + IERC20 balancerBPT; + IERC20 prl; + IWrappedNative weth; + IPermit2 permit2; + } + + /// @notice The Permit2 contract. + IPermit2 public immutable PERMIT2; + /// @notice The Balancer V3 router. + IBalancerV3Router public immutable BALANCER_ROUTER; + /// @notice The PRL token. + IERC20 public immutable PRL; + /// @notice The WETH token. + IWrappedNative public immutable WETH; + /// @notice The BPT token. + IERC20 public immutable BPT; + /// @notice Whether the pair is reversed. + bool public immutable isReversedBalancerPair; + + //------------------------------------------- + // Events + //------------------------------------------- + + /// @notice Event emitted when a user withdraws PRL and WETH for multiple requests. + /// @param requestIds The IDs of the withdrawal requests. + /// @param user The address of the user. + /// @param prlAmount The amount of PRL received. + /// @param wethAmount The amount of WETH received. + /// @param slashBptAmount The amount of BPT sent to the fee receiver. + event WithdrawlPRLAndWeth( + uint256[] requestIds, address user, uint256 prlAmount, uint256 wethAmount, uint256 slashBptAmount + ); + + /// @notice Event emitted when a user withdraws BPT for multiple requests. + /// @param requestIds The IDs of the withdrawal requests. + /// @param user The address of the user. + /// @param bptAmount The amount of BPT received. + /// @param slashBptAmount The amount of BPT sent to the fee receiver. + event WithdrawlBPT(uint256[] requestIds, address user, uint256 bptAmount, uint256 slashBptAmount); + + //------------------------------------------- + // Constructor + //------------------------------------------- + + /// @notice Constructor for the sPRL2V2 contract. + /// @param _feeReceiver The address of the fee receiver. + /// @param _accessManager The address of the access manager. + /// @param _startPenaltyPercentage The start penalty percentage. + /// @param _timeLockDuration The time lock duration. + /// @param _configParams The configuration parameters for the contract. + constructor( + address _feeReceiver, + address _accessManager, + uint256 _startPenaltyPercentage, + uint64 _timeLockDuration, + BPTConfigParams memory _configParams + ) + TimeLockPenaltyERC20( + NAME, + SYMBOL, + address(_configParams.balancerBPT), + _feeReceiver, + _accessManager, + _startPenaltyPercentage, + _timeLockDuration + ) + { + BALANCER_ROUTER = _configParams.balancerRouter; + PRL = _configParams.prl; + WETH = _configParams.weth; + BPT = _configParams.balancerBPT; + PERMIT2 = _configParams.permit2; + isReversedBalancerPair = address(_configParams.weth) > address(_configParams.prl); + + /// @dev Approve PERMIT2 to spend PRL and WETH + PRL.approve(address(PERMIT2), type(uint256).max); + WETH.approve(address(PERMIT2), type(uint256).max); + } + + //------------------------------------------- + // External Functions + //------------------------------------------- + + /// @notice Deposit BPT and mint the equivalent amount of sPRL2V2. + /// @param _amount The amount of BPT to deposit. + function depositBPT(uint256 _amount) external whenNotPaused nonReentrant { + BPT.safeTransferFrom(msg.sender, address(this), _amount); + _deposit(_amount); + } + + /// @notice Deposit PRL and WETH. + /// @param _maxPrlAmount The maximum amount of PRL to deposit. + /// @param _maxWethAmount The maximum amount of WETH to deposit. + /// @param _exactBptAmount The exact amount of BPT to mint. + /// @param _deadline The deadline for the permit. + /// @param _v The v parameter for the permit. + /// @param _r The r parameter for the permit. + /// @param _s The s parameter for the permit. + function depositPRLAndWeth( + uint256 _maxPrlAmount, + uint256 _maxWethAmount, + uint256 _exactBptAmount, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) + external + whenNotPaused + nonReentrant + returns (uint256[] memory amountsIn, uint256 bptAmount) + { + // @dev using try catch to avoid reverting the transaction in case of front-running + try IERC20Permit(address(PRL)).permit(msg.sender, address(this), _maxPrlAmount, _deadline, _v, _r, _s) { } + catch { } + + PRL.safeTransferFrom(msg.sender, address(this), _maxPrlAmount); + WETH.transferFrom(msg.sender, address(this), _maxWethAmount); + + (amountsIn, bptAmount) = _joinPool(_maxPrlAmount, _maxWethAmount, _exactBptAmount, false); + + _deposit(bptAmount); + } + + /// @notice Deposit PRL and ETH. + /// @param _maxPrlAmount The maximum amount of PRL to deposit. + /// @param _exactBptAmount The exact amount of BPT to mint. + /// @param _deadline The deadline for the permit. + /// @param _v The v parameter for the permit. + /// @param _r The r parameter for the permit. + /// @param _s The s parameter for the permit. + function depositPRLAndEth( + uint256 _maxPrlAmount, + uint256 _exactBptAmount, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) + external + payable + whenNotPaused + nonReentrant + returns (uint256[] memory amountsIn, uint256 bptAmount) + { + // @dev using try catch to avoid reverting the transaction in case of front-running + try IERC20Permit(address(PRL)).permit(msg.sender, address(this), _maxPrlAmount, _deadline, _v, _r, _s) { } + catch { } + + PRL.safeTransferFrom(msg.sender, address(this), _maxPrlAmount); + + (amountsIn, bptAmount) = _joinPool(_maxPrlAmount, msg.value, _exactBptAmount, true); + + _deposit(bptAmount); + } + + /// @notice Withdraw BPT for multiple requests. + /// @param _requestIds The request IDs to withdraw from. + /// @param _maxAcceptablePenalty The maximum penalty percentage to apply. + function withdrawBPT(uint256[] calldata _requestIds, uint256 _maxAcceptablePenalty) external nonReentrant { + (uint256 totalBptAmount, uint256 totalBptAmountSlashed) = _withdrawMultiple(_requestIds, _maxAcceptablePenalty); + if (totalBptAmountSlashed > 0) { + BPT.safeTransfer(feeReceiver, totalBptAmountSlashed); + } + emit WithdrawlBPT(_requestIds, msg.sender, totalBptAmount, totalBptAmountSlashed); + BPT.safeTransfer(msg.sender, totalBptAmount); + } + + /// @notice Withdraw PRL and WETH for multiple requests. + /// @param _requestIds The request IDs to withdraw from. + /// @param _minPrlAmount The minimum amount of PRL to receive. + /// @param _minWethAmount The minimum amount of WETH to receive. + /// @return prlAmount The amount of PRL received. + /// @return wethAmount The amount of WETH received. + function withdrawPRLAndWeth( + uint256[] calldata _requestIds, + uint256 _minPrlAmount, + uint256 _minWethAmount, + uint256 _maxPenaltyPercentage + ) + external + nonReentrant + returns (uint256 prlAmount, uint256 wethAmount) + { + (uint256 totalBptAmount, uint256 totalBptAmountSlashed) = _withdrawMultiple(_requestIds, _maxPenaltyPercentage); + + (prlAmount, wethAmount) = _exitPool(totalBptAmount, totalBptAmountSlashed, _minPrlAmount, _minWethAmount); + + emit WithdrawlPRLAndWeth(_requestIds, msg.sender, prlAmount, wethAmount, totalBptAmountSlashed); + PRL.safeTransfer(msg.sender, prlAmount); + WETH.transfer(msg.sender, wethAmount); + } + + /// @notice Allow users to emergency withdraw assets without penalties. + /// @dev This function can only be called when the contract is paused. + /// @param _amount The amount of assets to unlock. + function emergencyWithdraw(uint256 _amount) external whenPaused nonReentrant { + _burn(msg.sender, _amount); + emit EmergencyWithdraw(msg.sender, _amount); + BPT.safeTransfer(msg.sender, _amount); + } + + /// @notice Allow ETH to be received. + receive() external payable { } + + //------------------------------------------- + // Internal Functions + //------------------------------------------- + + /// @notice Join the pool. + /// @param _maxPrlAmount The maximum amount of PRL to deposit. + /// @param _maxEthAmount The maximum amount of ETH to deposit. + /// @param _exactBptAmount The exact amount of BPT to mint. + /// @param _isEth Whether the ETH is being deposited by the user. + /// @return amountsIn The amounts of PRL and ETH deposited. + /// @return bptAmount The amount of BPT received. + function _joinPool( + uint256 _maxPrlAmount, + uint256 _maxEthAmount, + uint256 _exactBptAmount, + bool _isEth + ) + internal + returns (uint256[] memory amountsIn, uint256 bptAmount) + { + uint256[] memory maxAmountsIn = new uint256[](2); + (maxAmountsIn[0], maxAmountsIn[1]) = + isReversedBalancerPair ? (_maxPrlAmount, _maxEthAmount) : (_maxEthAmount, _maxPrlAmount); + + /// @dev Approve tokens. + PERMIT2.approve(address(PRL), address(BALANCER_ROUTER), uint160(_maxPrlAmount), 0); + PERMIT2.approve(address(WETH), address(BALANCER_ROUTER), uint160(_maxEthAmount), 0); + + /// @dev Wrap ETH. + if (_isEth) { + WETH.deposit{ value: _maxEthAmount }(); + } + /// @dev Deposit into Balancer V3 + uint256[] memory _amountsIn = + BALANCER_ROUTER.addLiquidityProportional(address(BPT), maxAmountsIn, _exactBptAmount, false, ""); + + /// @dev Return any remaining PRL. + (uint256 prlAmountToReturn, uint256 ethAmountToReturn) = isReversedBalancerPair + ? (_maxPrlAmount - _amountsIn[0], _maxEthAmount - _amountsIn[1]) + : (_maxPrlAmount - _amountsIn[1], _maxEthAmount - _amountsIn[0]); + + if (prlAmountToReturn > 0) { + PRL.transfer(msg.sender, prlAmountToReturn); + } + + /// @dev Return any remaining WETH. + if (ethAmountToReturn > 0) { + if (_isEth) { + WETH.withdraw(ethAmountToReturn); + payable(msg.sender).sendValue(ethAmountToReturn); + } else { + WETH.transfer(msg.sender, ethAmountToReturn); + } + } + + return (_amountsIn, _exactBptAmount); + } + + /// @notice Exit the pool. + /// @dev Balancer V3 will revert if the amount of tokens received is less than the minimum expected. + /// @param _bptAmount The amount of BPT to withdraw. + /// @param _minPrlAmount The minimum amount of PRL to receive. + /// @param _minWethAmount The minimum amount of WETH to receive. + /// @return _prlAmount The amount of PRL received. + /// @return _wethAmount The amount of WETH received. + function _exitPool( + uint256 _bptAmount, + uint256 _bptAmountSlashed, + uint256 _minPrlAmount, + uint256 _minWethAmount + ) + internal + returns (uint256 _prlAmount, uint256 _wethAmount) + { + if (_bptAmountSlashed > 0) { + BPT.safeTransfer(feeReceiver, _bptAmountSlashed); + } + + uint256[] memory minAmountsOut = new uint256[](2); + (minAmountsOut[0], minAmountsOut[1]) = + isReversedBalancerPair ? (_minPrlAmount, _minWethAmount) : (_minWethAmount, _minPrlAmount); + + BPT.approve(address(BALANCER_ROUTER), _bptAmount); + + (uint256[] memory amountsOut) = + BALANCER_ROUTER.removeLiquidityProportional(address(BPT), _bptAmount, minAmountsOut, false, ""); + + (_prlAmount, _wethAmount) = + isReversedBalancerPair ? (amountsOut[0], amountsOut[1]) : (amountsOut[1], amountsOut[0]); + } +} diff --git a/deploy/config/arbiSepolia/config.json b/deploy/config/base/arbiSepolia/config.json similarity index 82% rename from deploy/config/arbiSepolia/config.json rename to deploy/config/base/arbiSepolia/config.json index 27fe1db..4552ebc 100644 --- a/deploy/config/arbiSepolia/config.json +++ b/deploy/config/base/arbiSepolia/config.json @@ -2,11 +2,10 @@ "isMainChainFeeDistributor": false, "accessManager": "0xE2A0FA5A1e7315e6b89b12Ace2541A6a0d3E5370", "wallets": { - "dao": "0x3676b6ed4ae0c9ce3E0034510719fdF5fd585d55" + "dao": "0x66CcEC236bbcE5f1bBd1b2fC0a4014e57A55ad02" }, "tokens": { "prl": "0x0F4DE51EF1E7611aD0A9181F2d62B9c8408EFfa2", - "paUSD": "0x0000000000000000000000000000000000000000", "par": "0x78C48A7d7Fc69735fDab448fe6068bbA44a920E6", "weth": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" }, @@ -30,8 +29,8 @@ "feeReceiver": "dao", "startPenaltyPercentage": "1000000000000000000", "timeLockDuration": 86400000, - "balancerV3Vault": "0x0000000000000000000000000000000000000000", - "auraVault": "0x0000000000000000000000000000000000000000", + "balancerV3Router": "0x0000000000000000000000000000000000000000", + "auraRewardsPool": "0x0000000000000000000000000000000000000000", "auraBoosterLite": "0x0000000000000000000000000000000000000000", "auraBPT": "0x0000000000000000000000000000000000000000", "balancerBPT": "0x0000000000000000000000000000000000000000" diff --git a/deploy/config/base/config.json b/deploy/config/base/config.json new file mode 100644 index 0000000..1f78628 --- /dev/null +++ b/deploy/config/base/config.json @@ -0,0 +1,40 @@ +{ + "isMainChainFeeDistributor": false, + "accessManager": "0x0e4e7Ca9D7b1e6293D0713EFEfB4BCA010DeBF46", + "wallets": { + "dao": "0xC5201FFE258a95Af986E7cD1fcaD54f3f63f2C95" + }, + "tokens": { + "prl": "0xfD28f108e95f4D41daAE9dbfFf707D677985998E", + "par": "0x0000000000000000000000000000000000000000", + "weth": "0x0000000000000000000000000000000000000000" + }, + "rewardMerkleDistributor": { + "token": "par", + "expiredRewardsRecipient": "dao" + }, + "feeDistributor": { + "mainChain": "polygon", + "destinationReceiver": "0x0000000000000000000000000000000000000000", + "feeToken": "par", + "bridgeableToken": "0x0000000000000000000000000000000000000000" + }, + "sprl1": { + "underlying": "prl", + "feeReceiver": "dao", + "startPenaltyPercentage": "500000000000000000", + "timeLockDuration": 2419200 + }, + "sprl2": { + "feeReceiver": "dao", + "permit2": "0x0000000000000000000000000000000000000000", + "startPenaltyPercentage": "500000000000000000", + "timeLockDuration": 2419200, + "balancerV3Router": "0x0000000000000000000000000000000000000000", + "auraRewardsPool": "0x0000000000000000000000000000000000000000", + "auraBoosterLite": "0x0000000000000000000000000000000000000000", + "auraBPT": "0x0000000000000000000000000000000000000000", + "balancerBPT": "0x0000000000000000000000000000000000000000", + "rewardsTokens": [] + } +} diff --git a/deploy/config/mainnet/config.json b/deploy/config/mainnet/config.json new file mode 100644 index 0000000..c94ef9b --- /dev/null +++ b/deploy/config/mainnet/config.json @@ -0,0 +1,40 @@ +{ + "isMainChainFeeDistributor": false, + "accessManager": "0x94Ea8800444017695345156319e96bdB1E355F7a", + "wallets": { + "dao": "0x25Fc7ffa8f9da3582a36633d04804F0004706F9b" + }, + "tokens": { + "prl": "0x6c0aeceeDc55c9d55d8B99216a670D85330941c3", + "par": "0x68037790a0229e9ce6eaa8a99ea92964106c4703", + "weth": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + }, + "rewardMerkleDistributor": { + "token": "par", + "expiredRewardsRecipient": "dao" + }, + "feeDistributor": { + "mainChain": "polygon", + "destinationReceiver": "0x90337e484B1Cb02132fc150d3Afa262147348545", + "feeToken": "par", + "bridgeableToken": "0x74e3f9D724Da6D7e4db8EcAb4f18e6ba44f60e30" + }, + "sprl1": { + "underlying": "prl", + "feeReceiver": "dao", + "startPenaltyPercentage": "500000000000000000", + "timeLockDuration": 2419200 + }, + "sprl2": { + "feeReceiver": "dao", + "permit2": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "startPenaltyPercentage": "500000000000000000", + "timeLockDuration": 60, + "balancerV3Router": "0x5C6fb490BDFD3246EB0bB062c168DeCAF4bD9FDd", + "auraRewardsPool": "0x486Be73794Ec19f3e9cF57f06f03fDCf7F0A9da4", + "auraBoosterLite": "0xA57b8d98dAE62B26Ec3bcC4a365338157060B234", + "auraBPT": "0x486Be73794Ec19f3e9cF57f06f03fDCf7F0A9da4", + "balancerBPT": "0x1846C6cBE0D433e152fA358e5fF27968E18bcE7c", + "rewardsTokens": ["0xba100000625a3754423978a60c9317c58a424e3d", "0xC0c293ce456fF0ED870ADd98a0828Dd4d2903DBF"] + } +} diff --git a/deploy/config/polygon/config.json b/deploy/config/polygon/config.json new file mode 100644 index 0000000..456dcc8 --- /dev/null +++ b/deploy/config/polygon/config.json @@ -0,0 +1,40 @@ +{ + "isMainChainFeeDistributor": true, + "accessManager": "0x7Df74BBB6F82eC1BCB1562a30ef5Bf5c326e2811", + "wallets": { + "dao": "0x2046c0416A558C40cb112E5ebB0Ca764c3C5c32a" + }, + "tokens": { + "prl": "0x7790dd69aa10eD3f1271E41CD7222D2a7d2D5948", + "par": "0xE2Aa7db6dA1dAE97C5f5C6914d285fBfCC32A128", + "weth": "0x0000000000000000000000000000000000000000" + }, + "rewardMerkleDistributor": { + "token": "par", + "expiredRewardsRecipient": "dao" + }, + "feeDistributor": { + "mainChain": "polygon", + "destinationReceiver": "0x0000000000000000000000000000000000000000", + "feeToken": "par", + "bridgeableToken": "0xE462Df8d4Aa7Db3aeCfe810ec8D1fa4f716BC9a8" + }, + "sprl1": { + "underlying": "prl", + "feeReceiver": "dao", + "startPenaltyPercentage": "500000000000000000", + "timeLockDuration": 2419200 + }, + "sprl2": { + "feeReceiver": "dao", + "permit2": "0x0000000000000000000000000000000000000000", + "startPenaltyPercentage": "500000000000000000", + "timeLockDuration": 2419200, + "balancerV3Router": "0x0000000000000000000000000000000000000000", + "auraRewardsPool": "0x0000000000000000000000000000000000000000", + "auraBoosterLite": "0x0000000000000000000000000000000000000000", + "auraBPT": "0x0000000000000000000000000000000000000000", + "balancerBPT": "0x0000000000000000000000000000000000000000", + "rewardsTokens": [] + } +} diff --git a/deploy/config/sepolia/config.json b/deploy/config/sepolia/config.json index 508f4d0..7a3c1cd 100644 --- a/deploy/config/sepolia/config.json +++ b/deploy/config/sepolia/config.json @@ -1,12 +1,11 @@ { "isMainChainFeeDistributor": true, - "accessManager": "0xe29F44Aa83EB42BA823719EA1D2d84861f72A5D0", + "accessManager": "0xfD28f108e95f4D41daAE9dbfFf707D677985998E", "wallets": { - "dao": "0x3676b6ed4ae0c9ce3E0034510719fdF5fd585d55" + "dao": "0x66CcEC236bbcE5f1bBd1b2fC0a4014e57A55ad02" }, "tokens": { "prl": "0xfaD382eE9B8229fb9B05DF9F9391baC5DD81f346", - "paUSD": "0x0000000000000000000000000000000000000000", "par": "0x68E88c802F146eAD2f99F3A91Fb880D1A2509672", "weth": "0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9" }, @@ -30,9 +29,8 @@ "feeReceiver": "dao", "startPenaltyPercentage": "1000000000000000000", "timeLockDuration": 2419200, - "balancerV3Vault": "0x0000000000000000000000000000000000000000", - "auraVault": "0x0000000000000000000000000000000000000000", - "auraVaultRewardToken": "0x0000000000000000000000000000000000000000", + "balancerV3Router": "0x0000000000000000000000000000000000000000", + "auraRewardsPool": "0x0000000000000000000000000000000000000000", "auraBoosterLite": "0x0000000000000000000000000000000000000000", "auraBPT": "0x0000000000000000000000000000000000000000", "balancerBPT": "0x0000000000000000000000000000000000000000" diff --git a/deploy/config/sonic/config.json b/deploy/config/sonic/config.json new file mode 100644 index 0000000..9b510d8 --- /dev/null +++ b/deploy/config/sonic/config.json @@ -0,0 +1,40 @@ +{ + "isMainChainFeeDistributor": false, + "accessManager": "0x8eFb3DED78FbaEF2a4eFe01E01BBD911E4094b78", + "wallets": { + "dao": "0xC5201FFE258a95Af986E7cD1fcaD54f3f63f2C95" + }, + "tokens": { + "prl": "0xfD28f108e95f4D41daAE9dbfFf707D677985998E", + "par": "0x0000000000000000000000000000000000000000", + "weth": "0x0000000000000000000000000000000000000000" + }, + "rewardMerkleDistributor": { + "token": "par", + "expiredRewardsRecipient": "dao" + }, + "feeDistributor": { + "mainChain": "polygon", + "destinationReceiver": "0x0000000000000000000000000000000000000000", + "feeToken": "par", + "bridgeableToken": "0x0000000000000000000000000000000000000000" + }, + "sprl1": { + "underlying": "prl", + "feeReceiver": "dao", + "startPenaltyPercentage": "500000000000000000", + "timeLockDuration": 2419200 + }, + "sprl2": { + "feeReceiver": "dao", + "permit2": "0x0000000000000000000000000000000000000000", + "startPenaltyPercentage": "500000000000000000", + "timeLockDuration": 2419200, + "balancerV3Router": "0x0000000000000000000000000000000000000000", + "auraRewardsPool": "0x0000000000000000000000000000000000000000", + "auraBoosterLite": "0x0000000000000000000000000000000000000000", + "auraBPT": "0x0000000000000000000000000000000000000000", + "balancerBPT": "0x0000000000000000000000000000000000000000", + "rewardsTokens": [] + } +} diff --git a/deploy/scripts/fees/MainFeeDistributor.ts b/deploy/scripts/fees/MainFeeDistributor.ts index cc0cf3e..b88aec9 100644 --- a/deploy/scripts/fees/MainFeeDistributor.ts +++ b/deploy/scripts/fees/MainFeeDistributor.ts @@ -1,44 +1,37 @@ import assert from "assert"; +import { deployScript, artifacts } from "@rocketh"; -import { ethers } from "ethers"; -import { type DeployFunction } from "hardhat-deploy/types"; - -import { checkAddressValid, GAS, getTokenAddressFromConfig, isAddressValid } from "../../utils"; +import { checkAddressValid, getTokenAddressFromConfig } from "../../utils"; import { readFileSync } from "fs"; import { ConfigData } from "../../utils/types"; const contractName = "MainFeeDistributor"; -const deploy: DeployFunction = async (hre) => { - const { getNamedAccounts, deployments } = hre; - - const { deploy } = deployments; - const { deployer } = await getNamedAccounts(); - - assert(deployer, "Missing deployer account"); - - console.log(`Network: ${hre.network.name}`); - console.log(`Deployer: ${deployer}`); - const config: ConfigData = JSON.parse(readFileSync(`./deploy/config/${hre.network.name}/config.json`).toString()); - if (!config.isMainChainFeeDistributor) throw new Error("MainFeeDistributor must be deployed on main chain"); +export default deployScript( + async ({ namedAccounts, name, deploy }) => { + const { deployer } = namedAccounts; + assert(deployer, "Missing deployer account"); + console.log(`Network: ${name}\nDeployer: ${deployer}\nDeploying: ${contractName}`); - const accessManager = checkAddressValid(config.accessManager, "access manager"); + const config: ConfigData = JSON.parse(readFileSync(`./deploy/config/${name}/config.json`).toString()); + if (!config.isMainChainFeeDistributor) throw new Error("MainFeeDistributor must be deployed on main chain"); - const feeDistributorData = config.feeDistributor; - const bridgeableToken = checkAddressValid(feeDistributorData.bridgeableToken, "bridgeable token"); + const accessManager = checkAddressValid(config.accessManager, "access manager"); - const feeToken = getTokenAddressFromConfig(feeDistributorData.feeToken, config); + const feeDistributorData = config.feeDistributor; + const bridgeableToken = checkAddressValid(feeDistributorData.bridgeableToken, "bridgeable token"); - const contract = await deploy(contractName, { - from: deployer, - args: [accessManager, bridgeableToken, feeToken], - log: true, - skipIfAlreadyDeployed: false, - ...GAS, - }); + const feeToken = getTokenAddressFromConfig(feeDistributorData.feeToken, config); - console.log(`Deployed contract: ${contractName}, network: ${hre.network.name}, address: ${contract.address}`); -}; + const contract = await deploy(contractName, { + account: deployer, + artifact: artifacts.MainFeeDistributor, + args: [accessManager, bridgeableToken, feeToken], + }); -deploy.tags = [contractName]; -export default deploy; + console.log(`Deployed ${contractName}, network: ${name}, address: ${contract.address}`); + }, + { + tags: [contractName], + }, +); diff --git a/deploy/scripts/fees/SideChainFeeCollector.ts b/deploy/scripts/fees/SideChainFeeCollector.ts index 8508d2e..a33ec06 100644 --- a/deploy/scripts/fees/SideChainFeeCollector.ts +++ b/deploy/scripts/fees/SideChainFeeCollector.ts @@ -1,46 +1,39 @@ import assert from "assert"; +import { deployScript, artifacts } from "@rocketh"; -import { ethers } from "ethers"; -import { type DeployFunction } from "hardhat-deploy/types"; - -import { checkAddressValid, GAS, getLzEidReceiver, getTokenAddressFromConfig, isAddressValid } from "../../utils"; +import { checkAddressValid, getLzEidReceiver, getTokenAddressFromConfig } from "../../utils"; import { readFileSync } from "fs"; import { ConfigData } from "../../utils/types"; const contractName = "SideChainFeeCollector"; -const deploy: DeployFunction = async (hre) => { - const { getNamedAccounts, deployments } = hre; - - const { deploy } = deployments; - const { deployer } = await getNamedAccounts(); - - assert(deployer, "Missing deployer account"); - - console.log(`Network: ${hre.network.name}`); - console.log(`Deployer: ${deployer}`); - const config: ConfigData = JSON.parse(readFileSync(`./deploy/config/${hre.network.name}/config.json`).toString()); - if (config.isMainChainFeeDistributor) throw new Error("SideChainFeeCollector must be deployed on side chain"); +export default deployScript( + async ({ namedAccounts, name, deploy }) => { + const { deployer } = namedAccounts; + assert(deployer, "Missing deployer account"); + console.log(`Network: ${name}\nDeployer: ${deployer}\nDeploying: ${contractName}`); - const accessManager = checkAddressValid(config.accessManager, "access manager"); + const config: ConfigData = JSON.parse(readFileSync(`./deploy/config/${name}/config.json`).toString()); + if (config.isMainChainFeeDistributor) throw new Error("SideChainFeeCollector must be deployed on side chain"); - const feeDistributorData = config.feeDistributor; - const lzEidReceiver = getLzEidReceiver(feeDistributorData.mainChain); - const destinationReceiver = checkAddressValid(feeDistributorData.destinationReceiver, "destination receiver"); - const bridgeableToken = checkAddressValid(feeDistributorData.bridgeableToken, "bridgeable token"); + const accessManager = checkAddressValid(config.accessManager, "access manager"); - const feeToken = getTokenAddressFromConfig(feeDistributorData.feeToken, config); + const feeDistributorData = config.feeDistributor; + const lzEidReceiver = getLzEidReceiver(feeDistributorData.mainChain); + const destinationReceiver = checkAddressValid(feeDistributorData.destinationReceiver, "destination receiver"); + const bridgeableToken = checkAddressValid(feeDistributorData.bridgeableToken, "bridgeable token"); - const contract = await deploy(contractName, { - from: deployer, - args: [accessManager, lzEidReceiver, bridgeableToken, destinationReceiver, feeToken], - log: true, - skipIfAlreadyDeployed: false, - ...GAS, - }); + const feeToken = getTokenAddressFromConfig(feeDistributorData.feeToken, config); - console.log(`Deployed contract: ${contractName}, network: ${hre.network.name}, address: ${contract.address}`); -}; + const contract = await deploy(contractName, { + account: deployer, + artifact: artifacts.SideChainFeeCollector, + args: [accessManager, lzEidReceiver, bridgeableToken, destinationReceiver, feeToken], + }); -deploy.tags = [contractName]; -export default deploy; + console.log(`Deployed ${contractName}, network: ${name}, address: ${contract.address}`); + }, + { + tags: [contractName], + }, +); diff --git a/deploy/scripts/rewardMerkleDistributor/RewardMerkleDistributor.ts b/deploy/scripts/rewardMerkleDistributor/RewardMerkleDistributor.ts index 1faaaf7..41295cc 100644 --- a/deploy/scripts/rewardMerkleDistributor/RewardMerkleDistributor.ts +++ b/deploy/scripts/rewardMerkleDistributor/RewardMerkleDistributor.ts @@ -1,47 +1,40 @@ import assert from "assert"; +import { deployScript, artifacts } from "@rocketh"; -import { type DeployFunction } from "hardhat-deploy/types"; - -import { checkAddressValid, GAS, getTokenAddressFromConfig, getWalletAddressFromConfig } from "../../utils"; +import { checkAddressValid, getTokenAddressFromConfig, getWalletAddressFromConfig } from "../../utils"; import { readFileSync } from "fs"; import { ConfigData } from "../../utils/types"; const contractName = "RewardMerkleDistributor"; -const deploy: DeployFunction = async (hre) => { - const { getNamedAccounts, deployments } = hre; - - const { deploy } = deployments; - const { deployer } = await getNamedAccounts(); - - assert(deployer, "Missing deployer account"); - - console.log(`Network: ${hre.network.name}`); - console.log(`Deployer: ${deployer}`); - - const config: ConfigData = JSON.parse(readFileSync(`./deploy/config/${hre.network.name}/config.json`).toString()); +export default deployScript( + async ({ namedAccounts, name, deploy }) => { + const { deployer } = namedAccounts; + assert(deployer, "Missing deployer account"); + console.log(`Network: ${name}\nDeployer: ${deployer}\nDeploying: ${contractName}`); - const accessManager = checkAddressValid(config.accessManager, "access manager"); + const config: ConfigData = JSON.parse(readFileSync(`./deploy/config/${name}/config.json`).toString()); - const rewardMerkleDistributorData = config.rewardMerkleDistributor; + const accessManager = checkAddressValid(config.accessManager, "access manager"); - const token = getTokenAddressFromConfig(rewardMerkleDistributorData.token, config); + const rewardMerkleDistributorData = config.rewardMerkleDistributor; - const expiredRewardsRecipient = getWalletAddressFromConfig( - rewardMerkleDistributorData.expiredRewardsRecipient, - config, - ); + const token = getTokenAddressFromConfig(rewardMerkleDistributorData.token, config); - const contract = await deploy(contractName, { - from: deployer, - args: [accessManager, token, expiredRewardsRecipient], - log: true, - skipIfAlreadyDeployed: false, - ...GAS, - }); + const expiredRewardsRecipient = getWalletAddressFromConfig( + rewardMerkleDistributorData.expiredRewardsRecipient, + config, + ); - console.log(`Deployed contract: ${contractName}, network: ${hre.network.name}, address: ${contract.address}`); -}; + const contract = await deploy(contractName, { + account: deployer, + artifact: artifacts.RewardMerkleDistributor, + args: [accessManager, token, expiredRewardsRecipient], + }); -deploy.tags = [contractName]; -export default deploy; + console.log(`Deployed ${contractName}, network: ${name}, address: ${contract.address}`); + }, + { + tags: [contractName], + }, +); diff --git a/deploy/scripts/sprl/sPRL1.ts b/deploy/scripts/sprl/sPRL1.ts index 12124c1..021d706 100644 --- a/deploy/scripts/sprl/sPRL1.ts +++ b/deploy/scripts/sprl/sPRL1.ts @@ -1,43 +1,41 @@ import assert from "assert"; +import { deployScript, artifacts } from "@rocketh"; -import { ethers } from "ethers"; -import { type DeployFunction } from "hardhat-deploy/types"; - -import { GAS, getTokenAddressFromConfig, getWalletAddressFromConfig, isAddressValid } from "../../utils"; +import { getTokenAddressFromConfig, getWalletAddressFromConfig, isAddressValid } from "../../utils"; import { readFileSync } from "fs"; import { ConfigData } from "../../utils/types"; const contractName = "sPRL1"; -const deploy: DeployFunction = async (hre) => { - const { getNamedAccounts, deployments } = hre; - - const { deploy } = deployments; - const { deployer } = await getNamedAccounts(); - - assert(deployer, "Missing deployer account"); - - console.log(`Network: ${hre.network.name}`); - console.log(`Deployer: ${deployer}`); - - const config: ConfigData = JSON.parse(readFileSync(`./deploy/config/${hre.network.name}/config.json`).toString()); - const accessManager = config.accessManager; - if (!isAddressValid(accessManager)) throw new Error("Invalid access manager address"); - - const sprl1Data = config.sprl1; - const underlying = getTokenAddressFromConfig(sprl1Data.underlying, config); - const feeReceiver = getWalletAddressFromConfig(sprl1Data.feeReceiver, config); - - const contract = await deploy(contractName, { - from: deployer, - args: [underlying, feeReceiver, accessManager, sprl1Data.startPenaltyPercentage, sprl1Data.timeLockDuration], - log: true, - skipIfAlreadyDeployed: false, - ...GAS, - }); - - console.log(`Deployed contract: ${contractName}, network: ${hre.network.name}, address: ${contract.address}`); -}; - -deploy.tags = [contractName]; -export default deploy; +export default deployScript( + async ({ namedAccounts, name, deploy }) => { + const { deployer } = namedAccounts; + assert(deployer, "Missing deployer account"); + console.log(`Network: ${name}\nDeployer: ${deployer}\nDeploying: ${contractName}`); + + const config: ConfigData = JSON.parse(readFileSync(`./deploy/config/${name}/config.json`).toString()); + const accessManager = config.accessManager; + if (!isAddressValid(accessManager)) throw new Error("Invalid access manager address"); + + const sprl1Data = config.sprl1; + const underlying = getTokenAddressFromConfig(sprl1Data.underlying, config); + const feeReceiver = getWalletAddressFromConfig(sprl1Data.feeReceiver, config); + + const contract = await deploy(contractName, { + account: deployer, + artifact: artifacts.sPRL1, + args: [ + underlying, + feeReceiver, + accessManager, + BigInt(sprl1Data.startPenaltyPercentage), + BigInt(sprl1Data.timeLockDuration), + ], + }); + + console.log(`Deployed ${contractName}, network: ${name}, address: ${contract.address}`); + }, + { + tags: [contractName], + }, +); diff --git a/deploy/scripts/sprl/sPRL2.ts b/deploy/scripts/sprl/sPRL2.ts index 5c45610..61cc6d0 100644 --- a/deploy/scripts/sprl/sPRL2.ts +++ b/deploy/scripts/sprl/sPRL2.ts @@ -1,67 +1,62 @@ import assert from "assert"; +import { deployScript, artifacts } from "@rocketh"; -import { ethers } from "ethers"; -import { type DeployFunction } from "hardhat-deploy/types"; - -import { - checkAddressValid, - GAS, - getTokenAddressFromConfig, - getWalletAddressFromConfig, - isAddressValid, -} from "../../utils"; +import { checkAddressValid, getTokenAddressFromConfig, getWalletAddressFromConfig, isAddressValid } from "../../utils"; import { readFileSync } from "fs"; import { ConfigData } from "../../utils/types"; const contractName = "sPRL2"; -const deploy: DeployFunction = async (hre) => { - const { getNamedAccounts, deployments } = hre; - - const { deploy } = deployments; - const { deployer } = await getNamedAccounts(); - - assert(deployer, "Missing deployer account"); - - console.log(`Network: ${hre.network.name}`); - console.log(`Deployer: ${deployer}`); - - const config: ConfigData = JSON.parse(readFileSync(`./deploy/config/${hre.network.name}/config.json`).toString()); - const accessManager = config.accessManager; - if (!isAddressValid(accessManager)) throw new Error("Invalid access manager address"); - - const sprl2Data = config.sprl2; - const prlToken = getTokenAddressFromConfig("prl", config); - const wethToken = getTokenAddressFromConfig("weth", config); - - const auraBPT = checkAddressValid(sprl2Data.auraBPT, "Aura BPT"); - const auraBoosterLite = checkAddressValid(sprl2Data.auraBoosterLite, "Aura Booster Lite"); - const auraVault = checkAddressValid(sprl2Data.auraVault, "Aura Vault"); - const balancerBPT = checkAddressValid(sprl2Data.balancerBPT, "Balancer BPT"); - - const feeReceiver = getWalletAddressFromConfig(sprl2Data.feeReceiver, config); - - const contract = await deploy(contractName, { - from: deployer, - args: [ - auraBPT, - feeReceiver, - accessManager, - sprl2Data.startPenaltyPercentage, - sprl2Data.timeLockDuration, - balancerBPT, +export default deployScript( + async ({ namedAccounts, name, deploy }) => { + const { deployer } = namedAccounts; + assert(deployer, "Missing deployer account"); + console.log(`Network: ${name}\nDeployer: ${deployer}\nDeploying: ${contractName}`); + + const config: ConfigData = JSON.parse(readFileSync(`./deploy/config/${name}/config.json`).toString()); + const accessManager = config.accessManager; + if (!isAddressValid(accessManager)) throw new Error("Invalid access manager address"); + + const sprl2Data = config.sprl2; + const prlToken = getTokenAddressFromConfig("prl", config); + const wethToken = getTokenAddressFromConfig("weth", config); + + const permit2 = checkAddressValid(sprl2Data.permit2, "Permit2"); + const feeReceiver = getWalletAddressFromConfig(sprl2Data.feeReceiver, config); + const balancerRouter = checkAddressValid(sprl2Data.balancerV3Router, "Balancer V3 Router"); + const balancerBPT = checkAddressValid(sprl2Data.balancerBPT, "Balancer BPT"); + const auraBoosterLite = checkAddressValid(sprl2Data.auraBoosterLite, "Aura Booster Lite"); + const auraRewardsPool = checkAddressValid(sprl2Data.auraRewardsPool, "Aura Rewards Pool"); + const auraBPT = checkAddressValid(sprl2Data.auraBPT, "Aura BPT"); + const rewardsTokens = sprl2Data.rewardsTokens.map((token) => checkAddressValid(token, "Reward Token")); + + const BPTConfigParams = { + balancerRouter, auraBoosterLite, - auraVault, - prlToken, - wethToken, - ], - log: true, - skipIfAlreadyDeployed: false, - ...GAS, - }); - - console.log(`Deployed contract: ${contractName}, network: ${hre.network.name}, address: ${contract.address}`); -}; - -deploy.tags = [contractName]; -export default deploy; + auraRewardsPool, + balancerBPT, + prl: prlToken, + weth: wethToken, + rewardTokens: rewardsTokens, + permit2, + }; + + const contract = await deploy(contractName, { + account: deployer, + artifact: artifacts.sPRL2, + args: [ + auraBPT, + feeReceiver, + accessManager, + BigInt(sprl2Data.startPenaltyPercentage), + BigInt(sprl2Data.timeLockDuration), + BPTConfigParams, + ], + }); + + console.log(`Deployed ${contractName}, network: ${name}, address: ${contract.address}`); + }, + { + tags: [contractName], + }, +); diff --git a/deploy/scripts/sprl/sPRL2V2.ts b/deploy/scripts/sprl/sPRL2V2.ts new file mode 100644 index 0000000..85ad348 --- /dev/null +++ b/deploy/scripts/sprl/sPRL2V2.ts @@ -0,0 +1,54 @@ +import assert from "assert"; +import { deployScript, artifacts } from "@rocketh"; + +import { checkAddressValid, getTokenAddressFromConfig, getWalletAddressFromConfig, isAddressValid } from "../../utils"; +import { readFileSync } from "fs"; +import { ConfigData } from "../../utils/types"; + +const contractName = "sPRL2V2"; + +export default deployScript( + async ({ namedAccounts, name, deploy }) => { + const { deployer } = namedAccounts; + assert(deployer, "Missing deployer account"); + console.log(`Network: ${name}\nDeployer: ${deployer}\nDeploying: ${contractName}`); + + const config: ConfigData = JSON.parse(readFileSync(`./deploy/config/${name}/config.json`).toString()); + const accessManager = config.accessManager; + if (!isAddressValid(accessManager)) throw new Error("Invalid access manager address"); + + const sprl2v2Data = config.sprl2v2; + const prlToken = getTokenAddressFromConfig("prl", config); + const wethToken = getTokenAddressFromConfig("weth", config); + + const permit2 = checkAddressValid(sprl2v2Data.permit2, "Permit2"); + const feeReceiver = getWalletAddressFromConfig(sprl2v2Data.feeReceiver, config); + const balancerRouter = checkAddressValid(sprl2v2Data.balancerV3Router, "Balancer V3 Router"); + const balancerBPT = checkAddressValid(sprl2v2Data.balancerBPT, "Balancer BPT"); + + const BPTConfigParams = { + balancerRouter, + balancerBPT, + prl: prlToken, + weth: wethToken, + permit2, + }; + + const contract = await deploy(contractName, { + account: deployer, + artifact: artifacts.sPRL2V2, + args: [ + feeReceiver, + accessManager, + BigInt(sprl2v2Data.startPenaltyPercentage), + BigInt(sprl2v2Data.timeLockDuration), + BPTConfigParams, + ], + }); + + console.log(`Deployed ${contractName}, network: ${name}, address: ${contract.address}`); + }, + { + tags: [contractName], + }, +); diff --git a/deploy/utils/index.ts b/deploy/utils/index.ts index d6a0ef3..1cd7793 100644 --- a/deploy/utils/index.ts +++ b/deploy/utils/index.ts @@ -1,13 +1,7 @@ -import type { CallOptions } from "hardhat-deploy/types"; -import { ethers } from "ethers"; +import { isAddress, zeroAddress } from "viem"; import { Address, ConfigData } from "./types"; import { EndpointId } from "@layerzerolabs/lz-definitions"; -export const GAS: CallOptions = { - maxFeePerGas: "20000000000", // 20 gwei - maxPriorityFeePerGas: "1000000000", -}; - export const getLzEidReceiver = (mainchain: string) => { if (mainchain === "sepolia") return EndpointId.SEPOLIA_V2_TESTNET; if (mainchain === "arbiSepolia") return EndpointId.ARBSEP_V2_TESTNET; @@ -39,5 +33,5 @@ export const checkAddressValid = (address: Address, label: string) => { }; export const isAddressValid = (address: string) => { - return ethers.utils.isAddress(address) && ethers.constants.AddressZero !== address; + return isAddress(address) && zeroAddress !== address; }; diff --git a/deploy/utils/types.ts b/deploy/utils/types.ts index 8972662..83a22bf 100644 --- a/deploy/utils/types.ts +++ b/deploy/utils/types.ts @@ -1,7 +1,7 @@ -import type { BigNumberish } from "ethers"; - export type Address = `0x${string}`; +export type Numeric = string | number | bigint; + export type ConfigData = { isMainChainFeeDistributor: boolean; accessManager: Address; @@ -10,7 +10,6 @@ export type ConfigData = { }; tokens: { prl: Address; - paUSD: Address; par: Address; weth: Address; }; @@ -27,17 +26,27 @@ export type ConfigData = { sprl1: { underlying: Address; feeReceiver: string; - startPenaltyPercentage: BigNumberish; + startPenaltyPercentage: Numeric; timeLockDuration: number; }; sprl2: { + permit2: Address; feeReceiver: string; - startPenaltyPercentage: BigNumberish; + startPenaltyPercentage: Numeric; timeLockDuration: number; - balancerV3Vault: Address; - auraVault: Address; + balancerV3Router: Address; + auraRewardsPool: Address; auraBoosterLite: Address; auraBPT: Address; balancerBPT: Address; + rewardsTokens: Address[]; + }; + sprl2v2: { + permit2: Address; + feeReceiver: string; + startPenaltyPercentage: Numeric; + timeLockDuration: number; + balancerV3Router: Address; + balancerBPT: Address; }; }; diff --git a/docs/audits/Bailsec - Parallel - sPRL2V2 - 1st Report - June 2026.pdf b/docs/audits/Bailsec - Parallel - sPRL2V2 - 1st Report - June 2026.pdf new file mode 100644 index 0000000..9f78db4 Binary files /dev/null and b/docs/audits/Bailsec - Parallel - sPRL2V2 - 1st Report - June 2026.pdf differ diff --git a/docs/audits/Bailsec - Parallel - sPRL2V2 - Final Report - June 2026.pdf b/docs/audits/Bailsec - Parallel - sPRL2V2 - Final Report - June 2026.pdf new file mode 100644 index 0000000..733b767 Binary files /dev/null and b/docs/audits/Bailsec - Parallel - sPRL2V2 - Final Report - June 2026.pdf differ diff --git a/foundry.toml b/foundry.toml index 3319e43..c7a621d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -35,3 +35,4 @@ [rpc_endpoints] mainnet = "https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}" +polygon = "https://polygon-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}" \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index f4719b8..faa6bc8 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,35 +1,30 @@ import "dotenv/config"; -import "hardhat-deploy"; +import HardhatViem from "@nomicfoundation/hardhat-viem"; +import HardhatDeploy from "hardhat-deploy"; -import { HardhatUserConfig, HttpNetworkAccountsUserConfig } from "hardhat/types"; +import { HardhatUserConfig } from "hardhat/types/config"; import { getRpcURL } from "./utils/getRpcURL"; -import { getVerifyConfig } from "./utils/getVerifyConfig"; -const MNEMONIC = process.env.MNEMONIC; - -// If you prefer to be authenticated using a private key, set a PRIVATE_KEY environment variable const PRIVATE_KEY = process.env.PRIVATE_KEY; - -const accounts: HttpNetworkAccountsUserConfig | undefined = MNEMONIC - ? { mnemonic: MNEMONIC } - : PRIVATE_KEY - ? [PRIVATE_KEY] - : undefined; - -if (accounts == null) { - console.warn( - "Could not find MNEMONIC or PRIVATE_KEY environment variables. It will not be possible to execute transactions in your example.", - ); -} +if (!PRIVATE_KEY) throw new Error("PRIVATE_KEY is not set"); +const accounts = [PRIVATE_KEY]; const config: HardhatUserConfig = { + plugins: [HardhatViem, HardhatDeploy], + // Foundry tests live in `test/`; keep Hardhat from compiling them by pointing its + // Solidity sources at `contracts/` and its test dir at an unused path. + paths: { + sources: "contracts", + tests: "hardhat-test", + }, solidity: { compilers: [ { version: "0.8.25", settings: { + evmVersion: "cancun", optimizer: { enabled: true, runs: 10_000, @@ -40,29 +35,34 @@ const config: HardhatUserConfig = { }, networks: { mainnet: { - accounts, + type: "http", url: getRpcURL("mainnet"), - verify: getVerifyConfig("mainnet"), + accounts, }, sepolia: { - accounts, + type: "http", url: getRpcURL("sepolia"), - verify: getVerifyConfig("sepolia"), + accounts, }, arbiSepolia: { - accounts, + type: "http", url: getRpcURL("arbiSepolia"), - verify: getVerifyConfig("arbiSepolia"), + accounts, }, polygon: { - accounts, + type: "http", url: getRpcURL("polygon"), - verify: getVerifyConfig("polygon"), + accounts, }, - }, - namedAccounts: { - deployer: { - default: 0, + base: { + type: "http", + url: getRpcURL("base"), + accounts, + }, + sonic: { + type: "http", + url: getRpcURL("sonic"), + accounts, }, }, }; diff --git a/package.json b/package.json index d153e59..46ac4b7 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,13 @@ "description": "Parallel Tokenomics", "version": "1.0.0", "private": false, + "type": "module", "scripts": { "compile": "forge build && hardhat compile", - "deploy": "npx hardhat deploy", - "clean": "rm -rf cache out", + "deploy": "hardhat deploy", + "clean": "rm -rf cache out generated", "build": "forge build", + "size": "forge build --sizes", "lint": "bun run lint:sol && bun run prettier:check", "lint:sol": "forge fmt --check && bun solhint \"{script,contracts,test}/**/*.sol\"", "prettier:check": "prettier --check \"**/*.{json,md,yml}\" --ignore-path \".prettierignore\"", @@ -17,7 +19,7 @@ "test:coverage": "forge coverage --ir-minimum", "test:coverage:report": "forge coverage --ir-minimum --report lcov && lcov --remove lcov.info -o lcov.info \"test/*\" && genhtml lcov.info -o coverage", "prepare": "husky", - "verify": "npx hardhat etherscan-verify --sleep" + "verify": "rocketh-verify etherscan" }, "dependencies": { "@layerzerolabs/lz-evm-oapp-v2": "^3.0.14", @@ -27,16 +29,30 @@ "@commitlint/cli": "^19.5.0", "@commitlint/config-conventional": "^19.5.0", "@layerzerolabs/lz-definitions": "^3.0.36", - "@nomicfoundation/hardhat-foundry": "^1.1.2", + "@nomicfoundation/hardhat-viem": "^3.0.0", + "@rocketh/deploy": "^0.19.8", + "@rocketh/doc": "^0.19.7", + "@rocketh/export": "^0.19.8", + "@rocketh/proxy": "^0.19.14", + "@rocketh/read-execute": "^0.19.3", + "@rocketh/signer": "^0.19.3", + "@rocketh/verifier": "^0.19.7", + "@rocketh/viem": "^0.19.3", "@types/fs-extra": "^11.0.4", - "dotenv": "^16.4.5", + "@types/node": "^24.3.0", + "dotenv": "^17.2.1", "forge-std": "github:foundry-rs/forge-std#v1.9.5", - "fs-extra": "^11.2.0", - "hardhat-contract-sizer": "^2.10.0", - "hardhat-deploy": "^0.14.0", + "fs-extra": "^11.3.0", + "hardhat": "^3.0.3", + "hardhat-deploy": "^2.0.0-next.40", "husky": "^9.1.6", "murky": "github:dmfxyz/murky", - "prettier": "^3.3.3", - "solhint": "^5.0.3" + "prettier": "^3.5.3", + "rocketh": "^0.19.7", + "solhint": "^5.0.3", + "ts-node": "^10.9.2", + "tsx": "^4.20.5", + "typescript": "^5.9.2", + "viem": "^2.37.1" } } diff --git a/remappings.txt b/remappings.txt index 422f91d..8a15695 100644 --- a/remappings.txt +++ b/remappings.txt @@ -2,3 +2,4 @@ @forge-std/=node_modules/forge-std/src/ @layerzerolabs/=node_modules/@layerzerolabs/ @murky/=node_modules/murky/src/ +contracts/=contracts/ diff --git a/rocketh.ts b/rocketh.ts new file mode 100644 index 0000000..30ea11c --- /dev/null +++ b/rocketh.ts @@ -0,0 +1,38 @@ +// ------------------------------------------------------------------------------------------------ +// Typed Config +// ------------------------------------------------------------------------------------------------ +import type { UserConfig } from "rocketh/types"; + +export const config = { + accounts: { + deployer: { + default: 0, + }, + }, + data: {}, +} as const satisfies UserConfig; + +// ------------------------------------------------------------------------------------------------ +// Extensions available to the deploy scripts through the environment object. +// ------------------------------------------------------------------------------------------------ +import * as deployExtension from "@rocketh/deploy"; // provides a deploy function +import * as readExecuteExtension from "@rocketh/read-execute"; // provides read/execute functions +import * as deployProxyExtension from "@rocketh/proxy"; // provides deployViaProxy for proxy based contracts +import * as viemExtension from "@rocketh/viem"; // provides viem handles to clients and contracts +const extensions = { + ...deployExtension, + ...readExecuteExtension, + ...deployProxyExtension, + ...viemExtension, +}; + +// ------------------------------------------------------------------------------------------------ +// Re-export the generated artifacts so they are available from the @rocketh alias. +import * as artifacts from "./generated/artifacts"; +export { artifacts }; + +// ------------------------------------------------------------------------------------------------ +// Create the deployScript helper bound to our extensions and typed config. +import { setupDeployScripts } from "rocketh"; +const { deployScript } = setupDeployScripts(extensions); +export { deployScript }; diff --git a/scripts/Base.s.sol b/scripts/Base.s.sol new file mode 100644 index 0000000..d507b0c --- /dev/null +++ b/scripts/Base.s.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { Script, console2 } from "@forge-std/Script.sol"; + +import { IAccessManager } from "@openzeppelin/contracts/access/manager/IAccessManager.sol"; + +contract BaseScript is Script { + /// @dev The address of the transaction broadcaster. + address internal broadcaster; + + IAccessManager accessManager = IAccessManager(0x94Ea8800444017695345156319e96bdB1E355F7a); + + modifier broadcast() { + uint256 privateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(privateKey); + broadcaster = vm.addr(privateKey); + _; + vm.stopBroadcast(); + } +} + +library Roles { + uint64 constant GOVERNOR_ROLE = 10; + uint64 constant GOVERNOR_ROLE_TIMELOCK = 11; + uint64 constant GUARDIAN_ROLE = 20; + uint64 constant GUARDIAN_ROLE_TIMELOCK = 21; + uint64 constant KEEPER_ROLE = 30; + uint64 constant KEEPER_ROLE_TIMELOCK = 31; + uint64 constant EURp_MINTER_ROLE = 100; + uint64 constant EURp_MINTER_ROLE_TIMELOCK = 105; + uint64 constant USDp_MINTER_ROLE = 110; + uint64 constant USDp_MINTER_ROLE_TIMELOCK = 115; + uint64 constant ETHp_MINTER_ROLE = 120; + uint64 constant ETHp_MINTER_ROLE_TIMELOCK = 125; + uint64 constant BTCp_MINTER_ROLE = 130; + uint64 constant BTCp_MINTER_ROLE_TIMELOCK = 135; +} diff --git a/scripts/SetMainFeeDistributorRole.s.sol b/scripts/SetMainFeeDistributorRole.s.sol new file mode 100644 index 0000000..4fbcd98 --- /dev/null +++ b/scripts/SetMainFeeDistributorRole.s.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import "./Base.s.sol"; + +import { MainFeeDistributor } from "contracts/fees/MainFeeDistributor.sol"; +import { FeeCollectorCore } from "contracts/fees/FeeCollectorCore.sol"; + +contract SetMainFeeDistributorRole is BaseScript { + address mainFeeDistributor = 0x90337e484B1Cb02132fc150d3Afa262147348545; + + function run() public broadcast { + bytes4[] memory guardianSelectors = new bytes4[](3); + guardianSelectors[0] = FeeCollectorCore.pause.selector; + guardianSelectors[1] = FeeCollectorCore.unpause.selector; + guardianSelectors[2] = FeeCollectorCore.emergencyRescue.selector; + accessManager.setTargetFunctionRole(mainFeeDistributor, guardianSelectors, Roles.GUARDIAN_ROLE); + + bytes4[] memory governorSelectors = new bytes4[](2); + governorSelectors[0] = MainFeeDistributor.updateBridgeableToken.selector; + governorSelectors[1] = MainFeeDistributor.updateFeeReceivers.selector; + accessManager.setTargetFunctionRole(mainFeeDistributor, governorSelectors, Roles.GOVERNOR_ROLE); + } +} diff --git a/scripts/SetRewardMerkleDistributor.s.sol b/scripts/SetRewardMerkleDistributor.s.sol new file mode 100644 index 0000000..96140df --- /dev/null +++ b/scripts/SetRewardMerkleDistributor.s.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import "./Base.s.sol"; + +import { RewardMerkleDistributor } from "contracts/rewardMerkleDistributor/RewardMerkleDistributor.sol"; + +contract SetRewardMerkleDistributorRole is BaseScript { + address rewardMerkleDistributor = 0x7b54f3D993d3bcA077946034Ea710F9c07420C72; + + function run() public broadcast { + bytes4[] memory guardianSelectors = new bytes4[](3); + guardianSelectors[0] = RewardMerkleDistributor.pause.selector; + guardianSelectors[1] = RewardMerkleDistributor.unpause.selector; + guardianSelectors[2] = RewardMerkleDistributor.emergencyRescue.selector; + accessManager.setTargetFunctionRole(rewardMerkleDistributor, guardianSelectors, Roles.GUARDIAN_ROLE); + + bytes4[] memory governorSelectors = new bytes4[](1); + governorSelectors[0] = RewardMerkleDistributor.updateExpiredRewardsRecipient.selector; + accessManager.setTargetFunctionRole(rewardMerkleDistributor, governorSelectors, Roles.GOVERNOR_ROLE); + + bytes4[] memory keeperSelectors = new bytes4[](1); + keeperSelectors[0] = RewardMerkleDistributor.updateMerkleDrop.selector; + accessManager.setTargetFunctionRole(rewardMerkleDistributor, keeperSelectors, Roles.KEEPER_ROLE); + } +} diff --git a/scripts/SetSPRL1Role.s.sol b/scripts/SetSPRL1Role.s.sol new file mode 100644 index 0000000..00043a1 --- /dev/null +++ b/scripts/SetSPRL1Role.s.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import "./Base.s.sol"; + +import { TimeLockPenaltyERC20 } from "contracts/sPRL/TimeLockPenaltyERC20.sol"; + +contract SetSPRL1Role is BaseScript { + address sprl1 = 0x7Df74BBB6F82eC1BCB1562a30ef5Bf5c326e2811; + + function run() public broadcast { + bytes4[] memory guardianSelectors = new bytes4[](2); + guardianSelectors[0] = TimeLockPenaltyERC20.pause.selector; + guardianSelectors[1] = TimeLockPenaltyERC20.unpause.selector; + accessManager.setTargetFunctionRole(sprl1, guardianSelectors, Roles.GUARDIAN_ROLE); + + bytes4[] memory governorSelectors = new bytes4[](3); + governorSelectors[0] = TimeLockPenaltyERC20.updateFeeReceiver.selector; + governorSelectors[1] = TimeLockPenaltyERC20.updateStartPenaltyPercentage.selector; + governorSelectors[2] = TimeLockPenaltyERC20.updateTimeLockDuration.selector; + accessManager.setTargetFunctionRole(sprl1, governorSelectors, Roles.GOVERNOR_ROLE); + } +} diff --git a/scripts/SetSPRL2Role.s.sol b/scripts/SetSPRL2Role.s.sol new file mode 100644 index 0000000..0a3d99a --- /dev/null +++ b/scripts/SetSPRL2Role.s.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import "./Base.s.sol"; + +import { sPRL2 } from "contracts/sPRL/sPRL2.sol"; +import { TimeLockPenaltyERC20 } from "contracts/sPRL/TimeLockPenaltyERC20.sol"; + +contract SetSPRL2Role is BaseScript { + address sprl2 = 0x90337e484B1Cb02132fc150d3Afa262147348545; + + function run() public broadcast { + bytes4[] memory guardianSelectors = new bytes4[](2); + guardianSelectors[0] = TimeLockPenaltyERC20.pause.selector; + guardianSelectors[1] = TimeLockPenaltyERC20.unpause.selector; + console2.log("guardianSelectors", guardianSelectors); + // accessManager.setTargetFunctionRole(sprl2, guardianSelectors, Roles.GUARDIAN_ROLE); + + bytes4[] memory governorSelectors = new bytes4[](4); + governorSelectors[0] = TimeLockPenaltyERC20.updateFeeReceiver.selector; + governorSelectors[1] = TimeLockPenaltyERC20.updateStartPenaltyPercentage.selector; + governorSelectors[2] = TimeLockPenaltyERC20.updateTimeLockDuration.selector; + governorSelectors[3] = sPRL2.updateRewardTokens.selector; + console2.log("governorSelectors", governorSelectors); + // accessManager.setTargetFunctionRole(sprl2, governorSelectors, Roles.GOVERNOR_ROLE); + } +} diff --git a/scripts/SetSideChainFeeCollectorRole.s.sol b/scripts/SetSideChainFeeCollectorRole.s.sol new file mode 100644 index 0000000..0595cc8 --- /dev/null +++ b/scripts/SetSideChainFeeCollectorRole.s.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import "./Base.s.sol"; + +import { SideChainFeeCollector } from "contracts/fees/SideChainFeeCollector.sol"; +import { FeeCollectorCore } from "contracts/fees/FeeCollectorCore.sol"; + +contract SetSideChainFeeCollectorRole is BaseScript { + address sideChainFeeCollector = 0x2A4ABC8dcBE2f68E48dFc0db5784C71dB8d5B89c; + + function run() public broadcast { + bytes4[] memory guardianSelectors = new bytes4[](3); + guardianSelectors[0] = FeeCollectorCore.pause.selector; + guardianSelectors[1] = FeeCollectorCore.unpause.selector; + guardianSelectors[2] = FeeCollectorCore.emergencyRescue.selector; + accessManager.setTargetFunctionRole(sideChainFeeCollector, guardianSelectors, Roles.GUARDIAN_ROLE); + + bytes4[] memory governorSelectors = new bytes4[](2); + governorSelectors[0] = SideChainFeeCollector.updateDestinationReceiver.selector; + governorSelectors[1] = SideChainFeeCollector.updateBridgeableToken.selector; + accessManager.setTargetFunctionRole(sideChainFeeCollector, governorSelectors, Roles.GOVERNOR_ROLE); + + bytes4[] memory keeperSelectors = new bytes4[](1); + keeperSelectors[0] = SideChainFeeCollector.swapLzToken.selector; + accessManager.setTargetFunctionRole(sideChainFeeCollector, keeperSelectors, Roles.KEEPER_ROLE); + } +} diff --git a/test/Base.t.sol b/test/Base.t.sol index 2f132e8..901a176 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -22,8 +22,6 @@ abstract contract Base_Test is Test, Deploys, Assertions, Defaults, Utils { // Deploy PAR token contract. par = _deployERC20Mock("PAR", "PAR", 18); - // Deploy paUSD token contract. - paUSD = _deployERC20Mock("paUSD", "paUSD", 18); // Deploy PRL token contract. prl = _deployERC20Mock("prl", "PRL", 18); // Deploy BPT token contract. @@ -62,7 +60,6 @@ abstract contract Base_Test is Test, Deploys, Assertions, Defaults, Utils { vm.deal({ account: user.addr, newBalance: INITIAL_BALANCE }); if (setTokenBalance) { par.mint(user.addr, INITIAL_BALANCE); - paUSD.mint(user.addr, INITIAL_BALANCE); prl.mint(user.addr, INITIAL_BALANCE); weth.mint(user.addr, INITIAL_BALANCE); } @@ -81,11 +78,7 @@ abstract contract Base_Test is Test, Deploys, Assertions, Defaults, Utils { address owner = vm.addr(privateKey); deadline = block.timestamp + 1 days; SigUtils.Permit memory permit = SigUtils.Permit({ - owner: owner, - spender: spender, - value: amount, - nonce: IERC20Permit(token).nonces(owner), - deadline: deadline + owner: owner, spender: spender, value: amount, nonce: IERC20Permit(token).nonces(owner), deadline: deadline }); bytes32 digest = sigUtils.getTypedDataHash(permit); diff --git a/test/Integrations.t.sol b/test/Integrations.t.sol index f784bcf..ae048a3 100644 --- a/test/Integrations.t.sol +++ b/test/Integrations.t.sol @@ -53,6 +53,16 @@ abstract contract Integrations_Test is Base_Test { }) ); + sprl2v2 = _deploySPRL2V2( + users.daoTreasury.addr, + address(accessManager), + DEFAULT_PENALTY_PERCENTAGE, + DEFAULT_TIME_LOCK_DURATION, + sPRL2V2.BPTConfigParams({ + balancerRouter: balancerV3RouterMock, balancerBPT: bpt, prl: prl, weth: weth, permit2: permit2 + }) + ); + rewardMerkleDistributor = _deployRewardMerkleDistributor(address(accessManager), address(par), users.daoTreasury.addr); } diff --git a/test/forks/Fork.t.sol b/test/forks/Fork.t.sol index ed0e485..c2c648c 100644 --- a/test/forks/Fork.t.sol +++ b/test/forks/Fork.t.sol @@ -9,8 +9,7 @@ abstract contract Fork_Test is Base_Test { address aura; function setUp() public virtual override { - // Fork Polygon Mainnet at a specific block number. - vm.createSelectFork({ blockNumber: 21_816_469, urlOrAlias: "mainnet" }); + vm.createSelectFork({ blockNumber: 24_905_586, urlOrAlias: "mainnet" }); // The base is set up after the fork is selected so that the base test contracts are deployed on the fork. Base_Test.setUp(); @@ -44,24 +43,24 @@ abstract contract Fork_Test is Base_Test { permit2 = Permit2Mock(0x000000000022D473030F116dDEE9F6B43aC78BA3); vm.label({ account: address(permit2), newLabel: "Permit2" }); - auraBpt = ERC20Mock(0x473dA6619e3bf97f946C6Cc991952c010e25eC3E); + auraBpt = ERC20Mock(0xC4a27dC753FF0513455D9A326d183220C078be8D); vm.label({ account: address(auraBpt), newLabel: "AuraBPT" }); balancerV3RouterMock = BalancerV3RouterMock(0x5C6fb490BDFD3246EB0bB062c168DeCAF4bD9FDd); - vm.label({ account: address(balancerV3RouterMock), newLabel: "BalancerV3RouterMock" }); + vm.label({ account: address(balancerV3RouterMock), newLabel: "BalancerV3Router" }); auraBoosterLiteMock = AuraBoosterLiteMock(0xA57b8d98dAE62B26Ec3bcC4a365338157060B234); - vm.label({ account: address(auraBoosterLiteMock), newLabel: "AuraBoosterLiteMock" }); - auraRewardPoolMock = AuraRewardPoolMock(0x473dA6619e3bf97f946C6Cc991952c010e25eC3E); - vm.label({ account: address(auraRewardPoolMock), newLabel: "AuraRewardPoolMock" }); + vm.label({ account: address(auraBoosterLiteMock), newLabel: "AuraBoosterLite" }); + auraRewardPoolMock = AuraRewardPoolMock(0x486Be73794Ec19f3e9cF57f06f03fDCf7F0A9da4); + vm.label({ account: address(auraRewardPoolMock), newLabel: "AuraRewardPool" }); bal = 0xba100000625a3754423978a60c9317c58a424e3D; vm.label({ account: address(bal), newLabel: "BAL" }); aura = 0xC0c293ce456fF0ED870ADd98a0828Dd4d2903DBF; vm.label({ account: address(aura), newLabel: "Aura" }); - bpt = ERC20Mock(0x5512fdDC40842b257e2A7742Be3DaDcf31574d53); + bpt = ERC20Mock(0x1846C6cBE0D433e152fA358e5fF27968E18bcE7c); vm.label({ account: address(bpt), newLabel: "BPT" }); - prl = ERC20Mock(0x04C154b66CB340F3Ae24111CC767e0184Ed00Cc6); + prl = ERC20Mock(0x6c0aeceeDc55c9d55d8B99216a670D85330941c3); vm.label({ account: address(prl), newLabel: "PRL" }); - weth = WrappedNativeMock(0x1e6ffa4e9F63d10B8820A3ab52566Af881Dab53c); + weth = WrappedNativeMock(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); vm.label({ account: address(weth), newLabel: "WETH" }); } } diff --git a/test/forks/ForkV2.t.sol b/test/forks/ForkV2.t.sol new file mode 100644 index 0000000..dda7756 --- /dev/null +++ b/test/forks/ForkV2.t.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; + +import "../Base.t.sol"; + +/// @notice Common logic needed by all sPRL2V2 fork tests. +abstract contract ForkV2_Test is Base_Test { + function setUp() public virtual override { + // Fork Mainnet at a specific block number. + vm.createSelectFork({ blockNumber: 24_905_586, urlOrAlias: "mainnet" }); + + // The base is set up after the fork is selected so that the base test contracts are deployed on the fork. + Base_Test.setUp(); + + _setForkContracts(); + + sprl2v2 = _deploySPRL2V2( + users.daoTreasury.addr, + address(accessManager), + DEFAULT_PENALTY_PERCENTAGE, + DEFAULT_TIME_LOCK_DURATION, + sPRL2V2.BPTConfigParams({ + balancerRouter: IBalancerV3Router(address(balancerV3RouterMock)), + balancerBPT: bpt, + prl: prl, + weth: weth, + permit2: permit2 + }) + ); + } + + function _setForkContracts() internal virtual { + permit2 = Permit2Mock(0x000000000022D473030F116dDEE9F6B43aC78BA3); + vm.label({ account: address(permit2), newLabel: "Permit2" }); + + balancerV3RouterMock = BalancerV3RouterMock(0x5C6fb490BDFD3246EB0bB062c168DeCAF4bD9FDd); + vm.label({ account: address(balancerV3RouterMock), newLabel: "BalancerV3Router" }); + + bpt = ERC20Mock(0x1846C6cBE0D433e152fA358e5fF27968E18bcE7c); + vm.label({ account: address(bpt), newLabel: "BPT" }); + prl = ERC20Mock(0x6c0aeceeDc55c9d55d8B99216a670D85330941c3); + vm.label({ account: address(prl), newLabel: "PRL" }); + weth = WrappedNativeMock(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + vm.label({ account: address(weth), newLabel: "WETH" }); + } +} diff --git a/test/forks/sPRL2V2/deposit.t.sol b/test/forks/sPRL2V2/deposit.t.sol new file mode 100644 index 0000000..47fb729 --- /dev/null +++ b/test/forks/sPRL2V2/deposit.t.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; + +import "test/forks/ForkV2.t.sol"; + +contract SPRL2V2_Deposits_Fork_Test is ForkV2_Test { + uint256 exactBptAmount = 1e18; + + function setUp() public override { + super.setUp(); + sigUtils = new SigUtils(prl.DOMAIN_SEPARATOR()); + + vm.startPrank(users.alice.addr); + deal(address(prl), address(users.alice.addr), INITIAL_BALANCE); + deal(address(weth), address(users.alice.addr), INITIAL_BALANCE); + } + + function test_fork_sPRL2V2_DepositBPT() external { + prl.approve(address(permit2), type(uint256).max); + weth.approve(address(permit2), type(uint256).max); + + uint48 deadline = uint48(block.timestamp + 15); + permit2.approve(address(prl), address(balancerV3RouterMock), uint160(INITIAL_BALANCE), deadline); + permit2.approve(address(weth), address(balancerV3RouterMock), uint160(INITIAL_BALANCE), deadline); + + balancerV3RouterMock.addLiquidityProportional( + address(bpt), _getMaxDepositAmountParams(INITIAL_BALANCE, INITIAL_BALANCE), exactBptAmount, false, "" + ); + assertEq(bpt.balanceOf(users.alice.addr), exactBptAmount); + + bpt.approve(address(sprl2v2), exactBptAmount); + sprl2v2.depositBPT(exactBptAmount); + assertEq(sprl2v2.balanceOf(users.alice.addr), exactBptAmount); + assertEq(bpt.balanceOf(users.alice.addr), 0); + } + + function test_fork_sPRL2V2_DepositPRLAndWeth() external { + weth.approve(address(sprl2v2), type(uint256).max); + + (uint256 deadline, uint8 v, bytes32 r, bytes32 s) = + _signPermitData(users.alice.privateKey, address(sprl2v2), INITIAL_BALANCE, address(prl)); + + (uint256[] memory amountsIn, uint256 bptAmount) = + sprl2v2.depositPRLAndWeth(INITIAL_BALANCE, INITIAL_BALANCE, exactBptAmount, deadline, v, r, s); + assertEq(bptAmount, exactBptAmount); + assertEq(INITIAL_BALANCE - amountsIn[0], prl.balanceOf(users.alice.addr)); + assertEq(INITIAL_BALANCE - amountsIn[1], weth.balanceOf(users.alice.addr)); + assertEq(bpt.balanceOf(users.alice.addr), 0); + assertEq(sprl2v2.balanceOf(users.alice.addr), exactBptAmount); + } + + function _getMaxDepositAmountParams( + uint256 maxPrlAmount, + uint256 maxWethAmount + ) + internal + view + returns (uint256[] memory maxAmountsIn) + { + maxAmountsIn = new uint256[](2); + (maxAmountsIn[0], maxAmountsIn[1]) = + address(weth) > address(prl) ? (maxPrlAmount, maxWethAmount) : (maxWethAmount, maxPrlAmount); + } +} diff --git a/test/forks/sPRL2V2/migrator.t.sol b/test/forks/sPRL2V2/migrator.t.sol new file mode 100644 index 0000000..570c275 --- /dev/null +++ b/test/forks/sPRL2V2/migrator.t.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; + +import "test/forks/ForkV2.t.sol"; +import { sPRL2 } from "contracts/sPRL/sPRL2.sol"; +import { sPRL2Migrator } from "contracts/sPRL/sPRL2Migrator.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IAccessManager } from "@openzeppelin/contracts/access/manager/IAccessManager.sol"; + +contract SPRL2Migrator_Fork_Test is ForkV2_Test { + sPRL2 sprl2Legacy; + sPRL2Migrator migrator; + + address admin = 0x25Fc7ffa8f9da3582a36633d04804F0004706F9b; + address user = address(0xBEEF); + + function setUp() public override { + super.setUp(); + + sprl2Legacy = sPRL2(payable(0xE8A2d848fE656E34A6caA35f375B42979e322135)); + vm.label(address(sprl2Legacy), "sPRL2Legacy"); + + migrator = new sPRL2Migrator(sprl2Legacy, sprl2v2); + + // User deposits into sPRL2 + uint256 depositAmount = 1e18; + vm.startPrank(user); + deal(address(prl), user, 100e18); + deal(address(weth), user, 100e18); + + IERC20(address(prl)).approve(address(permit2), type(uint256).max); + IERC20(address(weth)).approve(address(permit2), type(uint256).max); + + uint48 deadline = uint48(block.timestamp + 15); + permit2.approve(address(prl), address(balancerV3RouterMock), type(uint160).max, deadline); + permit2.approve(address(weth), address(balancerV3RouterMock), type(uint160).max, deadline); + + balancerV3RouterMock.addLiquidityProportional( + address(bpt), _getMaxDepositAmountParams(100e18, 100e18), depositAmount, false, "" + ); + + bpt.approve(address(sprl2Legacy), depositAmount); + sprl2Legacy.depositBPT(depositAmount); + vm.stopPrank(); + + // Admin pauses sPRL2 + vm.prank(admin); + sprl2Legacy.pause(); + } + + function test_fork_Migrator_Migrate() external { + uint256 amount = sprl2Legacy.balanceOf(user); + + vm.startPrank(user); + IERC20(address(sprl2Legacy)).approve(address(migrator), amount); + migrator.migrate(amount); + vm.stopPrank(); + + assertEq(sprl2Legacy.balanceOf(user), 0); + assertEq(sprl2v2.balanceOf(user), amount); + } + + function test_fork_Migrator_Migrate_Partial() external { + uint256 totalAmount = sprl2Legacy.balanceOf(user); + uint256 migrateAmount = totalAmount / 2; + + vm.startPrank(user); + IERC20(address(sprl2Legacy)).approve(address(migrator), migrateAmount); + migrator.migrate(migrateAmount); + vm.stopPrank(); + + assertEq(sprl2Legacy.balanceOf(user), totalAmount - migrateAmount); + assertEq(sprl2v2.balanceOf(user), migrateAmount); + } + + function _getMaxDepositAmountParams( + uint256 maxPrlAmount, + uint256 maxWethAmount + ) + internal + view + returns (uint256[] memory maxAmountsIn) + { + maxAmountsIn = new uint256[](2); + (maxAmountsIn[0], maxAmountsIn[1]) = + address(weth) > address(prl) ? (maxPrlAmount, maxWethAmount) : (maxWethAmount, maxPrlAmount); + } +} diff --git a/test/forks/sPRL2V2/withdraw.t.sol b/test/forks/sPRL2V2/withdraw.t.sol new file mode 100644 index 0000000..7353769 --- /dev/null +++ b/test/forks/sPRL2V2/withdraw.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; + +import "test/forks/ForkV2.t.sol"; + +contract SPRL2V2_Withdraw_Fork_Test is ForkV2_Test { + uint256 exactBptAmount = 1e18; + uint256 prlAmountDeposited; + uint256 wethAmountDeposited; + + function setUp() public override { + super.setUp(); + sigUtils = new SigUtils(prl.DOMAIN_SEPARATOR()); + + vm.startPrank(users.alice.addr); + deal(address(prl), address(users.alice.addr), INITIAL_BALANCE); + deal(address(weth), address(users.alice.addr), INITIAL_BALANCE); + weth.approve(address(sprl2v2), type(uint256).max); + + (uint256 deadline, uint8 v, bytes32 r, bytes32 s) = + _signPermitData(users.alice.privateKey, address(sprl2v2), INITIAL_BALANCE, address(prl)); + + (uint256[] memory amountsIn,) = + sprl2v2.depositPRLAndWeth(INITIAL_BALANCE, INITIAL_BALANCE, exactBptAmount, deadline, v, r, s); + prlAmountDeposited = amountsIn[0]; + wethAmountDeposited = amountsIn[1]; + } + + modifier requestWithdrawal() { + sprl2v2.requestWithdraw(exactBptAmount); + _; + } + + function test_fork_sPRL2V2_WithdrawBPT() external requestWithdrawal { + skip(sprl2v2.timeLockDuration()); + uint256 aliceBptBalance = sprl2v2.balanceOf(users.alice.addr); + uint256[] memory requestIds = new uint256[](1); + requestIds[0] = 0; + sprl2v2.withdrawBPT(requestIds, DEFAULT_PENALTY_PERCENTAGE); + assertEq(sprl2v2.balanceOf(users.alice.addr), 0); + assertEq(bpt.balanceOf(users.alice.addr), aliceBptBalance + exactBptAmount); + } + + function test_fork_sPRL2V2_WithdrawPRLAndWeth() external requestWithdrawal { + skip(sprl2v2.timeLockDuration()); + uint256 alicePrlBalance = prl.balanceOf(users.alice.addr); + uint256 aliceWethBalance = weth.balanceOf(users.alice.addr); + + uint256[] memory requestIds = new uint256[](1); + requestIds[0] = 0; + // Calculate the minimum amounts to withdraw 1% slippage + uint256 minPrlAmount = prlAmountDeposited * 99 / 100; + uint256 minWethAmount = wethAmountDeposited * 99 / 100; + + (uint256 prlAmount, uint256 wethAmount) = + sprl2v2.withdrawPRLAndWeth(requestIds, minPrlAmount, minWethAmount, DEFAULT_PENALTY_PERCENTAGE); + + assertEq(sprl2v2.balanceOf(users.alice.addr), 0); + assertEq(prl.balanceOf(users.alice.addr), alicePrlBalance + prlAmount); + assertEq(weth.balanceOf(users.alice.addr), aliceWethBalance + wethAmount); + } +} diff --git a/test/helpers/Deploys.sol b/test/helpers/Deploys.sol index 8e99edb..4067bf4 100644 --- a/test/helpers/Deploys.sol +++ b/test/helpers/Deploys.sol @@ -16,6 +16,7 @@ import { FeeCollectorCore } from "contracts/fees/FeeCollectorCore.sol"; import { sPRL1 } from "contracts/sPRL/sPRL1.sol"; import { sPRL2 } from "contracts/sPRL/sPRL2.sol"; +import { sPRL2V2 } from "contracts/sPRL/sPRL2V2.sol"; import { RewardMerkleDistributor } from "contracts/rewardMerkleDistributor/RewardMerkleDistributor.sol"; @@ -40,7 +41,6 @@ abstract contract Deploys is Test { ERC20Mock internal par; ERC20Mock internal prl; - ERC20Mock internal paUSD; ERC20Mock internal bpt; ERC20Mock internal auraBpt; @@ -66,6 +66,7 @@ abstract contract Deploys is Test { AccessManager internal accessManager; sPRL2 internal sprl2; + sPRL2V2 internal sprl2v2; sPRL1 internal sprl1; TimeLockPenaltyERC20Mock internal timeLockPenaltyERC20; @@ -237,4 +238,20 @@ abstract contract Deploys is Test { auraRewardPoolMock = new AuraRewardPoolMock(address(auraBoosterLiteMock)); vm.label({ account: address(auraRewardPoolMock), newLabel: "AuraRewardPoolMock" }); } + + function _deploySPRL2V2( + address _feeReceiver, + address _accessManager, + uint256 _startPenaltyPercentage, + uint64 _timeLockDuration, + sPRL2V2.BPTConfigParams memory _configParams + ) + internal + returns (sPRL2V2) + { + sPRL2V2 _sPRL2V2 = + new sPRL2V2(_feeReceiver, _accessManager, _startPenaltyPercentage, _timeLockDuration, _configParams); + vm.label({ account: address(_sPRL2V2), newLabel: "sPRL2V2" }); + return _sPRL2V2; + } } diff --git a/test/integrations/rewardMerkleDistributor/claim.t.sol b/test/integrations/rewardMerkleDistributor/claim.t.sol index c54a03f..d3557b4 100644 --- a/test/integrations/rewardMerkleDistributor/claim.t.sol +++ b/test/integrations/rewardMerkleDistributor/claim.t.sol @@ -51,6 +51,7 @@ contract RewardMerkleDistributor_Claim_Integrations_Test is Integrations_Test { startTime: uint64(block.timestamp) + firstEpochStartDelay, expiryTime: uint64(block.timestamp) + firstEpochStartDelay + epochLength * 6 }); + rewardMerkleDistributor.updateMerkleDrop(firstEpochId, firstEpochMerkleDrop); /// @dev Leaves are added in the order of the merkle tree's secondEpochLeaves. @@ -94,10 +95,7 @@ contract RewardMerkleDistributor_Claim_Integrations_Test is Integrations_Test { RewardMerkleDistributor.ClaimCallData[] memory claimsData = new RewardMerkleDistributor.ClaimCallData[](1); claimsData[0] = RewardMerkleDistributor.ClaimCallData({ - epochId: firstEpochId, - account: users.alice.addr, - amount: firstClaimRewardsAmount, - merkleProof: proof + epochId: firstEpochId, account: users.alice.addr, amount: firstClaimRewardsAmount, merkleProof: proof }); rewardMerkleDistributor.claims(claimsData); @@ -147,10 +145,7 @@ contract RewardMerkleDistributor_Claim_Integrations_Test is Integrations_Test { RewardMerkleDistributor.ClaimCallData[] memory claimsData = new RewardMerkleDistributor.ClaimCallData[](1); claimsData[0] = RewardMerkleDistributor.ClaimCallData({ - epochId: firstEpochId, - account: users.alice.addr, - amount: firstClaimRewardsAmount, - merkleProof: proof + epochId: firstEpochId, account: users.alice.addr, amount: firstClaimRewardsAmount, merkleProof: proof }); vm.warp(firstEpochMerkleDrop.expiryTime + 1); vm.expectRevert(RewardMerkleDistributor.EpochExpired.selector); @@ -163,10 +158,7 @@ contract RewardMerkleDistributor_Claim_Integrations_Test is Integrations_Test { RewardMerkleDistributor.ClaimCallData[] memory claimsData = new RewardMerkleDistributor.ClaimCallData[](1); claimsData[0] = RewardMerkleDistributor.ClaimCallData({ - epochId: secondEpochId, - account: users.alice.addr, - amount: secondClaimRewardsAmount, - merkleProof: proof + epochId: secondEpochId, account: users.alice.addr, amount: secondClaimRewardsAmount, merkleProof: proof }); vm.expectRevert(RewardMerkleDistributor.NotStarted.selector); rewardMerkleDistributor.claims(claimsData); @@ -179,10 +171,7 @@ contract RewardMerkleDistributor_Claim_Integrations_Test is Integrations_Test { RewardMerkleDistributor.ClaimCallData[] memory claimsData = new RewardMerkleDistributor.ClaimCallData[](1); claimsData[0] = RewardMerkleDistributor.ClaimCallData({ - epochId: firstEpochId, - account: users.alice.addr, - amount: firstClaimRewardsAmount, - merkleProof: proof + epochId: firstEpochId, account: users.alice.addr, amount: firstClaimRewardsAmount, merkleProof: proof }); rewardMerkleDistributor.claims(claimsData); @@ -198,10 +187,7 @@ contract RewardMerkleDistributor_Claim_Integrations_Test is Integrations_Test { RewardMerkleDistributor.ClaimCallData[] memory claimsData = new RewardMerkleDistributor.ClaimCallData[](1); claimsData[0] = RewardMerkleDistributor.ClaimCallData({ - epochId: firstEpochId, - account: users.alice.addr, - amount: firstClaimRewardsAmount * 100, - merkleProof: proof + epochId: firstEpochId, account: users.alice.addr, amount: firstClaimRewardsAmount * 100, merkleProof: proof }); vm.expectRevert(RewardMerkleDistributor.ProofInvalid.selector); @@ -232,10 +218,7 @@ contract RewardMerkleDistributor_Claim_Integrations_Test is Integrations_Test { RewardMerkleDistributor.ClaimCallData[] memory claimsData = new RewardMerkleDistributor.ClaimCallData[](1); claimsData[0] = RewardMerkleDistributor.ClaimCallData({ - epochId: firstEpochId, - account: users.alice.addr, - amount: firstClaimRewardsAmount, - merkleProof: proof + epochId: firstEpochId, account: users.alice.addr, amount: firstClaimRewardsAmount, merkleProof: proof }); vm.expectRevert(RewardMerkleDistributor.TotalEpochRewardsExceeded.selector); diff --git a/test/integrations/rewardMerkleDistributor/forwardExpiredRewards.t.sol b/test/integrations/rewardMerkleDistributor/forwardExpiredRewards.t.sol index 085212c..06ccb6b 100644 --- a/test/integrations/rewardMerkleDistributor/forwardExpiredRewards.t.sol +++ b/test/integrations/rewardMerkleDistributor/forwardExpiredRewards.t.sol @@ -120,10 +120,7 @@ contract RewardMerkleDistributor_ForwardExpiredRewards_Integrations_Test is Inte RewardMerkleDistributor.ClaimCallData[] memory claimsData = new RewardMerkleDistributor.ClaimCallData[](1); claimsData[0] = RewardMerkleDistributor.ClaimCallData({ - epochId: firstEpochId, - account: users.alice.addr, - amount: firstRewardsAmount, - merkleProof: proof + epochId: firstEpochId, account: users.alice.addr, amount: firstRewardsAmount, merkleProof: proof }); rewardMerkleDistributor.claims(claimsData); @@ -163,9 +160,7 @@ contract RewardMerkleDistributor_ForwardExpiredRewards_Integrations_Test is Inte rewardMerkleDistributor.forwardExpiredRewards(epochIds); } - function test_RewardMerkleDistributor_ForwardExpiredRewards_RevertWhen_EpochIsNotExpired_SeveralEpochs() - external - { + function test_RewardMerkleDistributor_ForwardExpiredRewards_RevertWhen_EpochIsNotExpired_SeveralEpochs() external { vm.startPrank(users.admin.addr); uint64[] memory epochIds = new uint64[](2); epochIds[0] = firstEpochId; diff --git a/test/integrations/sPRL2V2/cancelWithdrawalRequests.t.sol b/test/integrations/sPRL2V2/cancelWithdrawalRequests.t.sol new file mode 100644 index 0000000..8f04342 --- /dev/null +++ b/test/integrations/sPRL2V2/cancelWithdrawalRequests.t.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { Pausable } from "@openzeppelin/contracts/utils/Pausable.sol"; + +import "test/Integrations.t.sol"; + +contract SPRL2V2_CancelWithdrawalRequests_Integrations_Test is Integrations_Test { + function setUp() public override { + super.setUp(); + vm.startPrank(users.alice.addr); + + deal(address(bpt), address(users.alice.addr), INITIAL_BALANCE); + bpt.approve(address(sprl2v2), type(uint256).max); + sprl2v2.depositBPT(INITIAL_BALANCE); + } + + modifier requestOneWithdraw() { + sprl2v2.requestWithdraw(INITIAL_BALANCE); + _; + } + + function test_SPRL2V2_CancelWithdrawalRequests_SingleRequest() external requestOneWithdraw { + uint256[] memory ids = new uint256[](1); + ids[0] = 0; + vm.expectEmit(address(sprl2v2)); + emit TimeLockPenaltyERC20.WithdrawalRequestCancelled(ids[0], users.alice.addr, INITIAL_BALANCE); + sprl2v2.cancelWithdrawalRequests(ids); + assertEq(sprl2v2.balanceOf(users.alice.addr), INITIAL_BALANCE); + assertEq(sprl2v2.unlockingAmount(), 0); + + (uint256 requestAmount, uint64 requestTime, uint64 releaseTime, TimeLockPenaltyERC20.WITHDRAW_STATUS status) = + sprl2v2.userVsWithdrawals(users.alice.addr, ids[0]); + + assertEq(requestAmount, INITIAL_BALANCE); + assertEq(requestTime, block.timestamp); + assertEq(releaseTime, block.timestamp + sprl2v2.timeLockDuration()); + assertEq(status, TimeLockPenaltyERC20.WITHDRAW_STATUS.CANCELLED); + + uint256[] memory requestIds = sprl2v2.findUnlockingIDs(users.alice.addr, 0, false, 1); + assertEq(requestIds.length, 0); + } + + modifier requestMultiWithdraw() { + sprl2v2.requestWithdraw(1); + sprl2v2.requestWithdraw(2); + sprl2v2.requestWithdraw(3); + sprl2v2.requestWithdraw(4); + sprl2v2.requestWithdraw(5); + _; + } + + function test_SPRL2V2_CancelWithdrawalRequests_MultipleRequests() external requestMultiWithdraw { + uint256[] memory ids = new uint256[](3); + ids[0] = 0; + ids[1] = 2; + ids[2] = 4; + sprl2v2.cancelWithdrawalRequests(ids); + + assertEq(sprl2v2.balanceOf(users.alice.addr), INITIAL_BALANCE - 6); + assertEq(sprl2v2.unlockingAmount(), 6); + + uint256[] memory requestIds = sprl2v2.findUnlockingIDs(users.alice.addr, 0, false, 10); + assertEq(requestIds.length, 2); + assertEq(requestIds[0], 1); + assertEq(requestIds[1], 3); + } + + function test_SPRL2V2_CancelWithdrawalRequests_RevertWhen_WrongRequestStatus() external { + uint256[] memory ids = new uint256[](1); + ids[0] = 0; + vm.expectRevert(abi.encodeWithSelector(TimeLockPenaltyERC20.CannotCancelWithdrawalRequest.selector, ids[0])); + sprl2v2.cancelWithdrawalRequests(ids); + } +} diff --git a/test/integrations/sPRL2V2/constructor.t.sol b/test/integrations/sPRL2V2/constructor.t.sol new file mode 100644 index 0000000..2c037f8 --- /dev/null +++ b/test/integrations/sPRL2V2/constructor.t.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import "test/Integrations.t.sol"; + +contract SPRL2V2_Constructor_Integrations_Test is Integrations_Test { + function test_SPRL2V2_Constructor() external { + sprl2v2 = new sPRL2V2( + users.daoTreasury.addr, + address(accessManager), + DEFAULT_PENALTY_PERCENTAGE, + DEFAULT_TIME_LOCK_DURATION, + sPRL2V2.BPTConfigParams({ + balancerRouter: IBalancerV3Router(address(balancerV3RouterMock)), + balancerBPT: IERC20(address(bpt)), + prl: IERC20(address(prl)), + weth: IWrappedNative(address(weth)), + permit2: IPermit2(address(permit2)) + }) + ); + assertEq(sprl2v2.authority(), address(accessManager)); + assertEq(address(sprl2v2.underlying()), address(bpt)); + assertEq(sprl2v2.timeLockDuration(), DEFAULT_TIME_LOCK_DURATION); + assertEq(sprl2v2.startPenaltyPercentage(), DEFAULT_PENALTY_PERCENTAGE); + assertEq(sprl2v2.unlockingAmount(), 0); + assertEq(sprl2v2.feeReceiver(), users.daoTreasury.addr); + assertEq(sprl2v2.name(), "Stake 20WETH-80PRL Deposit Vault"); + assertEq(sprl2v2.symbol(), "sPRL2V2"); + assertEq(address(sprl2v2.BALANCER_ROUTER()), address(balancerV3RouterMock)); + assertEq(address(sprl2v2.BPT()), address(bpt)); + assertEq(address(sprl2v2.PRL()), address(prl)); + assertEq(address(sprl2v2.WETH()), address(weth)); + assertEq(address(sprl2v2.PERMIT2()), address(permit2)); + } + + function test_SPRL2V2_Constructor_RevertWhen_FeeReceiverZeroAddress() external { + vm.expectRevert(TimeLockPenaltyERC20.FeeReceiverZeroAddress.selector); + new sPRL2V2( + address(0), + address(accessManager), + DEFAULT_PENALTY_PERCENTAGE, + DEFAULT_TIME_LOCK_DURATION, + sPRL2V2.BPTConfigParams({ + balancerRouter: IBalancerV3Router(address(balancerV3RouterMock)), + balancerBPT: IERC20(address(bpt)), + prl: IERC20(address(prl)), + weth: IWrappedNative(address(weth)), + permit2: IPermit2(address(permit2)) + }) + ); + } +} diff --git a/test/integrations/sPRL2V2/deposit.t.sol b/test/integrations/sPRL2V2/deposit.t.sol new file mode 100644 index 0000000..8577d84 --- /dev/null +++ b/test/integrations/sPRL2V2/deposit.t.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import "test/Integrations.t.sol"; + +contract SPRL2V2_Deposits_Integrations_Test is Integrations_Test { + uint256 exactBptAmount = 1e18; + + function setUp() public override { + super.setUp(); + sigUtils = new SigUtils(prl.DOMAIN_SEPARATOR()); + + vm.startPrank(users.alice.addr); + weth.approve(address(sprl2v2), type(uint256).max); + } + + function test_sPRL2V2_DepositBPT() external { + deal(address(bpt), address(users.alice.addr), INITIAL_BALANCE); + bpt.approve(address(sprl2v2), INITIAL_BALANCE); + vm.expectEmit(address(sprl2v2)); + emit TimeLockPenaltyERC20.Deposited(users.alice.addr, INITIAL_BALANCE); + sprl2v2.depositBPT(INITIAL_BALANCE); + + assertEq(bpt.balanceOf(address(users.alice.addr)), 0); + assertEq(sprl2v2.balanceOf(address(users.alice.addr)), INITIAL_BALANCE); + } + + function test_sPRL2V2_DepositPRLAndWeth(uint256 maxPrlAmount, uint256 maxWethAmount) external { + maxPrlAmount = bound(maxPrlAmount, 1, prl.balanceOf(users.alice.addr)); + maxWethAmount = bound(maxWethAmount, 1, weth.balanceOf(users.alice.addr)); + + uint256 alicePrlBalanceBefore = prl.balanceOf(users.alice.addr); + uint256 aliceWethBalanceBefore = weth.balanceOf(users.alice.addr); + + (uint256 deadline, uint8 v, bytes32 r, bytes32 s) = + _signPermitData(users.alice.privateKey, address(sprl2v2), maxPrlAmount, address(prl)); + + vm.expectEmit(address(sprl2v2)); + emit TimeLockPenaltyERC20.Deposited(users.alice.addr, exactBptAmount); + sprl2v2.depositPRLAndWeth(maxPrlAmount, maxWethAmount, exactBptAmount, deadline, v, r, s); + + assertEq(prl.balanceOf(users.alice.addr), alicePrlBalanceBefore - maxPrlAmount); + assertEq(weth.balanceOf(users.alice.addr), aliceWethBalanceBefore - maxWethAmount); + assertEq(sprl2v2.balanceOf(users.alice.addr), exactBptAmount); + } + + function test_sPRL2V2_DepositPRLAndEth(uint256 maxPrlAmount, uint256 maxEthAmount) external { + maxPrlAmount = bound(maxPrlAmount, 1, prl.balanceOf(users.alice.addr)); + maxEthAmount = bound(maxEthAmount, 1, (users.alice.addr).balance); + + uint256 alicePrlBalanceBefore = prl.balanceOf(users.alice.addr); + uint256 aliceEthBalanceBefore = (users.alice.addr).balance; + + (uint256 deadline, uint8 v, bytes32 r, bytes32 s) = + _signPermitData(users.alice.privateKey, address(sprl2v2), maxPrlAmount, address(prl)); + + vm.expectEmit(address(sprl2v2)); + emit TimeLockPenaltyERC20.Deposited(users.alice.addr, exactBptAmount); + sprl2v2.depositPRLAndEth{ value: maxEthAmount }(maxPrlAmount, exactBptAmount, deadline, v, r, s); + assertEq(prl.balanceOf(users.alice.addr), alicePrlBalanceBefore - maxPrlAmount); + assertEq((users.alice.addr).balance, aliceEthBalanceBefore - maxEthAmount); + assertEq(sprl2v2.balanceOf(users.alice.addr), exactBptAmount); + } + + function test_sPRL2V2_Deposit_ReturnRemainingTokens_ETH(uint256 maxPrlAmount, uint256 maxEthAmount) external { + maxPrlAmount = bound(maxPrlAmount, 1, prl.balanceOf(users.alice.addr)); + maxEthAmount = bound(maxEthAmount, 1, (users.alice.addr).balance); + + uint256 alicePrlBalanceBefore = prl.balanceOf(users.alice.addr); + uint256 aliceEthBalanceBefore = (users.alice.addr).balance; + + // @dev should use half of the WETH and half of the PRL + balancerV3RouterMock.updateRatio(0.5e18); + + (uint256 deadline, uint8 v, bytes32 r, bytes32 s) = + _signPermitData(users.alice.privateKey, address(sprl2v2), maxPrlAmount, address(prl)); + + sprl2v2.depositPRLAndEth{ value: maxEthAmount }(maxPrlAmount, exactBptAmount, deadline, v, r, s); + assertEq(prl.balanceOf(users.alice.addr), alicePrlBalanceBefore - maxPrlAmount / 2); + assertEq((users.alice.addr).balance, aliceEthBalanceBefore - maxEthAmount / 2); + assertEq(sprl2v2.balanceOf(users.alice.addr), exactBptAmount); + assertEq(prl.balanceOf(address(sprl2v2)), 0); + assertEq(weth.balanceOf(address(sprl2v2)), 0); + } + + function test_sPRL2V2_Deposit_ReturnRemainingTokens_WETH(uint256 maxPrlAmount, uint256 maxWethAmount) external { + maxPrlAmount = bound(maxPrlAmount, 1, prl.balanceOf(users.alice.addr)); + maxWethAmount = bound(maxWethAmount, 1, weth.balanceOf(users.alice.addr)); + + uint256 alicePrlBalanceBefore = prl.balanceOf(users.alice.addr); + uint256 aliceWethBalanceBefore = weth.balanceOf(users.alice.addr); + + // @dev should use half of the WETH and half of the PRL + balancerV3RouterMock.updateRatio(0.5e18); + + (uint256 deadline, uint8 v, bytes32 r, bytes32 s) = + _signPermitData(users.alice.privateKey, address(sprl2v2), maxPrlAmount, address(prl)); + + sprl2v2.depositPRLAndWeth(maxPrlAmount, maxWethAmount, exactBptAmount, deadline, v, r, s); + assertEq(prl.balanceOf(users.alice.addr), alicePrlBalanceBefore - maxPrlAmount / 2); + assertEq(weth.balanceOf(users.alice.addr), aliceWethBalanceBefore - maxWethAmount / 2); + assertEq(sprl2v2.balanceOf(users.alice.addr), exactBptAmount); + assertEq(prl.balanceOf(address(sprl2v2)), 0); + assertEq(weth.balanceOf(address(sprl2v2)), 0); + } +} diff --git a/test/integrations/sPRL2V2/migrator.t.sol b/test/integrations/sPRL2V2/migrator.t.sol new file mode 100644 index 0000000..4cf2292 --- /dev/null +++ b/test/integrations/sPRL2V2/migrator.t.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import "test/Integrations.t.sol"; +import { sPRL2Migrator } from "contracts/sPRL/sPRL2Migrator.sol"; + +contract SPRL2Migrator_Integrations_Test is Integrations_Test { + sPRL2Migrator migrator; + uint256 DEPOSIT_AMOUNT = 100e18; + + function setUp() public override { + super.setUp(); + sigUtils = new SigUtils(prl.DOMAIN_SEPARATOR()); + + migrator = new sPRL2Migrator(sprl2, sprl2v2); + + // Alice deposits into sPRL2 + vm.startPrank(users.alice.addr); + weth.approve(address(sprl2), type(uint256).max); + + (uint256 deadline, uint8 v, bytes32 r, bytes32 s) = + _signPermitData(users.alice.privateKey, address(sprl2), DEPOSIT_AMOUNT, address(prl)); + + sprl2.depositPRLAndWeth(DEPOSIT_AMOUNT, DEPOSIT_AMOUNT, DEPOSIT_AMOUNT, deadline, v, r, s); + vm.stopPrank(); + + // Admin pauses sPRL2 for migration + vm.prank(users.admin.addr); + sprl2.pause(); + } + + function test_Migrator_Constructor() external view { + assertEq(address(migrator.SPRL2()), address(sprl2)); + assertEq(address(migrator.SPRL2V2()), address(sprl2v2)); + assertEq(address(migrator.BPT()), address(bpt)); + } + + function test_Migrator_Migrate() external { + uint256 amount = sprl2.balanceOf(users.alice.addr); + + vm.startPrank(users.alice.addr); + IERC20(address(sprl2)).approve(address(migrator), amount); + + vm.expectEmit(address(migrator)); + emit sPRL2Migrator.Migrated(users.alice.addr, amount); + migrator.migrate(amount); + vm.stopPrank(); + + assertEq(sprl2.balanceOf(users.alice.addr), 0); + assertEq(sprl2v2.balanceOf(users.alice.addr), amount); + } + + function test_Migrator_Migrate_Partial() external { + uint256 totalAmount = sprl2.balanceOf(users.alice.addr); + uint256 migrateAmount = totalAmount / 2; + + vm.startPrank(users.alice.addr); + IERC20(address(sprl2)).approve(address(migrator), migrateAmount); + migrator.migrate(migrateAmount); + vm.stopPrank(); + + assertEq(sprl2.balanceOf(users.alice.addr), totalAmount - migrateAmount); + assertEq(sprl2v2.balanceOf(users.alice.addr), migrateAmount); + } + + function test_Migrator_Migrate_MultipleUsers() external { + // Bob deposits into sPRL2 + vm.startPrank(users.bob.addr); + weth.approve(address(sprl2), type(uint256).max); + + // sPRL2 is paused, so Bob needs to have deposited before pause + // Instead, unpause, deposit, then re-pause + vm.stopPrank(); + vm.prank(users.admin.addr); + sprl2.unpause(); + + vm.startPrank(users.bob.addr); + (uint256 deadline, uint8 v, bytes32 r, bytes32 s) = + _signPermitData(users.bob.privateKey, address(sprl2), DEPOSIT_AMOUNT, address(prl)); + sprl2.depositPRLAndWeth(DEPOSIT_AMOUNT, DEPOSIT_AMOUNT, DEPOSIT_AMOUNT, deadline, v, r, s); + vm.stopPrank(); + + vm.prank(users.admin.addr); + sprl2.pause(); + + // Alice migrates + uint256 aliceAmount = sprl2.balanceOf(users.alice.addr); + vm.startPrank(users.alice.addr); + IERC20(address(sprl2)).approve(address(migrator), aliceAmount); + migrator.migrate(aliceAmount); + vm.stopPrank(); + + // Bob migrates + uint256 bobAmount = sprl2.balanceOf(users.bob.addr); + vm.startPrank(users.bob.addr); + IERC20(address(sprl2)).approve(address(migrator), bobAmount); + migrator.migrate(bobAmount); + vm.stopPrank(); + + assertEq(sprl2.balanceOf(users.alice.addr), 0); + assertEq(sprl2v2.balanceOf(users.alice.addr), aliceAmount); + assertEq(sprl2.balanceOf(users.bob.addr), 0); + assertEq(sprl2v2.balanceOf(users.bob.addr), bobAmount); + } + + function test_Migrator_RevertWhen_AmountZero() external { + vm.prank(users.alice.addr); + vm.expectRevert(abi.encodeWithSelector(sPRL2Migrator.MigrationAmountZero.selector)); + migrator.migrate(0); + } + + function test_Migrator_RevertWhen_NotPaused() external { + vm.prank(users.admin.addr); + sprl2.unpause(); + + uint256 amount = sprl2.balanceOf(users.alice.addr); + vm.startPrank(users.alice.addr); + IERC20(address(sprl2)).approve(address(migrator), amount); + vm.expectRevert(abi.encodeWithSelector(Pausable.ExpectedPause.selector)); + migrator.migrate(amount); + } + + function test_Migrator_RevertWhen_InsufficientApproval() external { + vm.startPrank(users.alice.addr); + vm.expectRevert(); + migrator.migrate(DEPOSIT_AMOUNT); + } +} diff --git a/test/integrations/sPRL2V2/requestWithdraw.t.sol b/test/integrations/sPRL2V2/requestWithdraw.t.sol new file mode 100644 index 0000000..1465630 --- /dev/null +++ b/test/integrations/sPRL2V2/requestWithdraw.t.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { Pausable } from "@openzeppelin/contracts/utils/Pausable.sol"; + +import "test/Integrations.t.sol"; + +contract SPRL2V2_RequestWithdraw_Integrations_Test is Integrations_Test { + function setUp() public override { + super.setUp(); + vm.startPrank(users.alice.addr); + + deal(address(bpt), address(users.alice.addr), INITIAL_BALANCE); + bpt.approve(address(sprl2v2), type(uint256).max); + sprl2v2.depositBPT(INITIAL_BALANCE); + } + + function test_SPRL2V2_RequestWithdraw() external { + vm.expectEmit(address(sprl2v2)); + emit TimeLockPenaltyERC20.WithdrawalRequested(0, users.alice.addr, INITIAL_BALANCE); + sprl2v2.requestWithdraw(INITIAL_BALANCE); + + assertEq(sprl2v2.balanceOf(users.alice.addr), 0); + assertEq(sprl2v2.unlockingAmount(), INITIAL_BALANCE); + + (uint256 requestAmount, uint64 requestTime, uint64 releaseTime, TimeLockPenaltyERC20.WITHDRAW_STATUS status) = + sprl2v2.userVsWithdrawals(users.alice.addr, 0); + + assertEq(requestAmount, INITIAL_BALANCE); + assertEq(requestTime, block.timestamp); + assertEq(releaseTime, block.timestamp + sprl2v2.timeLockDuration()); + assertEq(status, TimeLockPenaltyERC20.WITHDRAW_STATUS.UNLOCKING); + + uint256[] memory requestIds = sprl2v2.findUnlockingIDs(users.alice.addr, 0, false, 1); + assertEq(requestIds.length, 1); + assertEq(requestIds[0], 0); + } + + function test_SPRL2V2_RequestWithdraw_RevertWhen_AmountZero() external { + vm.expectRevert(TimeLockPenaltyERC20.NullAmount.selector); + sprl2v2.requestWithdraw(0); + } + + function testFuzz_SPRL2V2_RequestWithdraw_Several(uint16 requestTime) external { + requestTime = _boundUint16(requestTime, 1, 100); + uint256 withdrawAmount = INITIAL_BALANCE / requestTime; + uint256 expectedAmount = withdrawAmount * requestTime; + for (uint16 i = 0; i < requestTime; i++) { + sprl2v2.requestWithdraw(withdrawAmount); + } + assertEq(sprl2v2.balanceOf(users.alice.addr), INITIAL_BALANCE - expectedAmount); + assertEq(sprl2v2.unlockingAmount(), expectedAmount); + + uint256[] memory requestIds = sprl2v2.findUnlockingIDs(users.alice.addr, 0, false, requestTime); + assertEq(requestIds.length, requestTime); + } +} diff --git a/test/integrations/sPRL2V2/updateFeeReceiver.t.sol b/test/integrations/sPRL2V2/updateFeeReceiver.t.sol new file mode 100644 index 0000000..cfc1088 --- /dev/null +++ b/test/integrations/sPRL2V2/updateFeeReceiver.t.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import "test/Integrations.t.sol"; + +contract SPRL2V2_UpdateFeeReceiver_Integrations_Test is Integrations_Test { + address internal newPaymentReceiver = makeAddr("newPaymentReceiver"); + + function test_sPRL2V2_UpdateFeeReceiver() external { + vm.startPrank(users.admin.addr); + vm.expectEmit(address(sprl2v2)); + emit TimeLockPenaltyERC20.FeeReceiverUpdated(newPaymentReceiver); + sprl2v2.updateFeeReceiver(newPaymentReceiver); + } + + function test_SPRL2V2_UpdateFeeReceiver_RevertWhen_CallerNotAuthorized() external { + vm.startPrank(users.hacker.addr); + vm.expectRevert(abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, users.hacker.addr)); + sprl2v2.updateFeeReceiver(newPaymentReceiver); + } + + function test_SPRL2V2_UpdateFeeReceiver_RevertWhen_ZeroAddress() external { + vm.startPrank(users.admin.addr); + vm.expectRevert(TimeLockPenaltyERC20.FeeReceiverZeroAddress.selector); + sprl2v2.updateFeeReceiver(address(0)); + } +} diff --git a/test/integrations/sPRL2V2/withdraw.t.sol b/test/integrations/sPRL2V2/withdraw.t.sol new file mode 100644 index 0000000..a07eecd --- /dev/null +++ b/test/integrations/sPRL2V2/withdraw.t.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import "test/Integrations.t.sol"; + +contract SPRL2V2_Withdraw_Integrations_Test is Integrations_Test { + uint256 WITHDRAW_AMOUNT = 1e18; + uint256 INITIAL_AMOUNT = 100e18; + + function setUp() public override { + super.setUp(); + sigUtils = new SigUtils(prl.DOMAIN_SEPARATOR()); + + deal(address(weth), INITIAL_BALANCE); + + vm.startPrank(users.alice.addr); + prl.approve(address(sprl2v2), type(uint256).max); + weth.approve(address(sprl2v2), type(uint256).max); + + (uint256 deadline, uint8 v, bytes32 r, bytes32 s) = + _signPermitData(users.alice.privateKey, address(sprl2v2), INITIAL_AMOUNT, address(prl)); + + sprl2v2.depositPRLAndWeth(INITIAL_AMOUNT, INITIAL_AMOUNT, INITIAL_AMOUNT, deadline, v, r, s); + } + + modifier requestSingleWithdraw() { + sprl2v2.requestWithdraw(WITHDRAW_AMOUNT); + _; + } + + function test_sPRL2V2_WithdrawBPT_SingleRequest() external requestSingleWithdraw { + assertEq(sprl2v2.balanceOf(users.alice.addr), INITIAL_AMOUNT - WITHDRAW_AMOUNT); + skip(sprl2v2.timeLockDuration()); + + uint256 aliceBalanceBefore = bpt.balanceOf(users.alice.addr); + uint256[] memory requestIds = new uint256[](1); + requestIds[0] = 0; + sprl2v2.withdrawBPT(requestIds, DEFAULT_PENALTY_PERCENTAGE); + + uint256 aliceBalanceAfter = bpt.balanceOf(users.alice.addr); + assertEq(aliceBalanceAfter - aliceBalanceBefore, WITHDRAW_AMOUNT); + } + + function test_sPRL2V2_WithdrawPRLAndWeth_SingleRequest() external requestSingleWithdraw { + assertEq(sprl2v2.balanceOf(users.alice.addr), INITIAL_AMOUNT - WITHDRAW_AMOUNT); + skip(sprl2v2.timeLockDuration()); + + uint256 alicePrlBalanceBefore = prl.balanceOf(users.alice.addr); + uint256 aliceWethBalanceBefore = weth.balanceOf(users.alice.addr); + + uint256[] memory requestIds = new uint256[](1); + requestIds[0] = 0; + sprl2v2.withdrawPRLAndWeth(requestIds, INITIAL_AMOUNT, INITIAL_AMOUNT, DEFAULT_PENALTY_PERCENTAGE); + + assertEq(prl.balanceOf(users.alice.addr), alicePrlBalanceBefore + INITIAL_AMOUNT); + assertEq(weth.balanceOf(users.alice.addr), aliceWethBalanceBefore + INITIAL_AMOUNT); + } + + modifier requestMultiWithdraw() { + sprl2v2.requestWithdraw(WITHDRAW_AMOUNT); + sprl2v2.requestWithdraw(WITHDRAW_AMOUNT); + sprl2v2.requestWithdraw(WITHDRAW_AMOUNT); + _; + } + + function test_sPRL2V2_WithdrawBPT_MultipleRequests() external requestMultiWithdraw { + uint256 withdrawAmount = WITHDRAW_AMOUNT * 3; + assertEq(sprl2v2.balanceOf(users.alice.addr), INITIAL_AMOUNT - withdrawAmount); + skip(sprl2v2.timeLockDuration()); + + uint256 aliceBalanceBefore = bpt.balanceOf(users.alice.addr); + uint256[] memory requestIds = new uint256[](3); + requestIds[0] = 0; + requestIds[1] = 1; + requestIds[2] = 2; + sprl2v2.withdrawBPT(requestIds, DEFAULT_PENALTY_PERCENTAGE); + + uint256 aliceBalanceAfter = bpt.balanceOf(users.alice.addr); + assertEq(aliceBalanceAfter - aliceBalanceBefore, withdrawAmount); + } + + function test_sPRL2V2_WithdrawPRLAndWeth_MultipleRequests() external requestMultiWithdraw { + uint256 withdrawAmount = WITHDRAW_AMOUNT * 3; + assertEq(sprl2v2.balanceOf(users.alice.addr), INITIAL_AMOUNT - withdrawAmount); + skip(sprl2v2.timeLockDuration()); + + uint256 alicePrlBalanceBefore = prl.balanceOf(users.alice.addr); + uint256 aliceWethBalanceBefore = weth.balanceOf(users.alice.addr); + uint256[] memory requestIds = new uint256[](3); + requestIds[0] = 0; + requestIds[1] = 1; + requestIds[2] = 2; + sprl2v2.withdrawPRLAndWeth(requestIds, withdrawAmount, withdrawAmount, DEFAULT_PENALTY_PERCENTAGE); + + assertEq(prl.balanceOf(users.alice.addr), alicePrlBalanceBefore + withdrawAmount); + assertEq(weth.balanceOf(users.alice.addr), aliceWethBalanceBefore + withdrawAmount); + } + + function test_sPRL2V2_WithdrawPRLAndWeth_BeforeUnLockTimeShouldSendUnderlyingTokenToFeeReceiver() + external + requestMultiWithdraw + { + skip(sprl2v2.timeLockDuration() / 2); + uint256 withdrawAmount = WITHDRAW_AMOUNT * 3; + uint256 expectedAmountAliceReceived = withdrawAmount / 2; + uint256 expectedAmountFeeReceiverReceived = withdrawAmount / 2; + uint256 alicePrlBalanceBefore = prl.balanceOf(users.alice.addr); + uint256 aliceWethBalanceBefore = weth.balanceOf(users.alice.addr); + uint256 aliceEthBalanceBefore = users.alice.addr.balance; + + uint256[] memory requestIds = new uint256[](3); + requestIds[0] = 0; + requestIds[1] = 1; + requestIds[2] = 2; + sprl2v2.withdrawPRLAndWeth( + requestIds, expectedAmountAliceReceived, expectedAmountAliceReceived, DEFAULT_PENALTY_PERCENTAGE + ); + + assertEq(bpt.balanceOf(address(users.daoTreasury.addr)), expectedAmountFeeReceiverReceived); + assertEq(sprl2v2.balanceOf(users.alice.addr), INITIAL_AMOUNT - withdrawAmount); + assertEq(prl.balanceOf(users.alice.addr), alicePrlBalanceBefore + expectedAmountAliceReceived); + assertEq(weth.balanceOf(users.alice.addr), aliceWethBalanceBefore + expectedAmountAliceReceived); + assertEq(users.alice.addr.balance, aliceEthBalanceBefore); + } + + function test_SPRL2V2_WithdrawBPT_MultipleRequests_RevertWhen_PenaltyPercentageExceedMaxAcceptance() + external + requestMultiWithdraw + { + uint256[] memory requestIds = new uint256[](1); + requestIds[0] = 0; + vm.expectRevert(abi.encodeWithSelector(TimeLockPenaltyERC20.MaxPenaltyPercentageExceeded.selector)); + sprl2v2.withdrawBPT(requestIds, DEFAULT_PENALTY_PERCENTAGE - 1); + } + + function test_SPRL2V2_WithdrawPRLAndWeth_MultipleRequests_RevertWhen_PenaltyPercentageExceedMaxAcceptance() + external + requestMultiWithdraw + { + uint256[] memory requestIds = new uint256[](1); + requestIds[0] = 0; + vm.expectRevert(abi.encodeWithSelector(TimeLockPenaltyERC20.MaxPenaltyPercentageExceeded.selector)); + sprl2v2.withdrawPRLAndWeth(requestIds, WITHDRAW_AMOUNT, WITHDRAW_AMOUNT, DEFAULT_PENALTY_PERCENTAGE - 1); + } + + modifier PauseContract() { + vm.startPrank(users.admin.addr); + sprl2v2.pause(); + _; + } + + function test_SPRL2V2_EmergencyWithdraw() external PauseContract { + vm.startPrank(users.alice.addr); + uint256 aliceBalanceBefore = bpt.balanceOf(users.alice.addr); + + sprl2v2.emergencyWithdraw(WITHDRAW_AMOUNT); + + uint256 aliceBalanceAfter = bpt.balanceOf(users.alice.addr); + assertEq(aliceBalanceAfter - aliceBalanceBefore, WITHDRAW_AMOUNT); + + assertEq(sprl2v2.balanceOf(users.daoTreasury.addr), 0); + } +} diff --git a/test/integrations/timeLockPenaltyERC20/recoverERC20.t.sol b/test/integrations/timeLockPenaltyERC20/recoverERC20.t.sol new file mode 100644 index 0000000..5fc4613 --- /dev/null +++ b/test/integrations/timeLockPenaltyERC20/recoverERC20.t.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import "test/Integrations.t.sol"; + +contract TimeLockPenaltyERC20_RecoverERC20_Integrations_Test is Integrations_Test { + uint256 internal constant REWARD_AMOUNT = 1e18; + + function setUp() public override { + super.setUp(); + // Simulate reward tokens being sent to the contract on its behalf (e.g. MERKL claim). + rewardToken.mint(address(timeLockPenaltyERC20), REWARD_AMOUNT); + } + + function test_TimeLockPenaltyERC20_RecoverERC20() external { + vm.startPrank(users.admin.addr); + vm.expectEmit(address(timeLockPenaltyERC20)); + emit TimeLockPenaltyERC20.TokensRecovered(address(rewardToken), REWARD_AMOUNT); + timeLockPenaltyERC20.recoverERC20(IERC20(address(rewardToken))); + + assertEq(rewardToken.balanceOf(address(timeLockPenaltyERC20)), 0); + assertEq(rewardToken.balanceOf(timeLockPenaltyERC20.feeReceiver()), REWARD_AMOUNT); + } + + function test_TimeLockPenaltyERC20_RecoverERC20_RevertWhen_TokenIsUnderlying() external { + // The mock's underlying is PRL (see Integrations setUp). + vm.startPrank(users.admin.addr); + vm.expectRevert(TimeLockPenaltyERC20.CannotRecoverUnderlying.selector); + timeLockPenaltyERC20.recoverERC20(IERC20(address(prl))); + } + + function test_TimeLockPenaltyERC20_RecoverERC20_RevertWhen_CallerNotAuthorized() external { + vm.startPrank(users.hacker.addr); + vm.expectRevert(abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, users.hacker.addr)); + timeLockPenaltyERC20.recoverERC20(IERC20(address(rewardToken))); + } +} diff --git a/test/mocks/BalancerV3RouterMock.sol b/test/mocks/BalancerV3RouterMock.sol index 36eee35..ffa4cc0 100644 --- a/test/mocks/BalancerV3RouterMock.sol +++ b/test/mocks/BalancerV3RouterMock.sol @@ -11,9 +11,11 @@ contract BalancerV3RouterMock is IBalancerV3Router { ERC20Mock public bpt; IPermit2 public permit2; uint256 ratio = 1e18; + bool isReversedBalancerPair; constructor(address[2] memory _tokens, address _bpt, address _permit2) { tokens = _tokens; + isReversedBalancerPair = _tokens[0] > _tokens[1]; bpt = ERC20Mock(_bpt); permit2 = IPermit2(_permit2); } @@ -36,8 +38,12 @@ contract BalancerV3RouterMock is IBalancerV3Router { { uint256 amountIn0 = (maxAmountsIn[0] * ratio) / 1e18; uint256 amountIn1 = (maxAmountsIn[1] * ratio) / 1e18; - permit2.transferFrom(msg.sender, address(this), uint160(amountIn0), tokens[0]); - permit2.transferFrom(msg.sender, address(this), uint160(amountIn1), tokens[1]); + permit2.transferFrom( + msg.sender, address(this), uint160(amountIn0), isReversedBalancerPair ? tokens[1] : tokens[0] + ); + permit2.transferFrom( + msg.sender, address(this), uint160(amountIn1), isReversedBalancerPair ? tokens[0] : tokens[1] + ); bpt.mint(msg.sender, exactBptAmountOut); amountsIn = new uint256[](2); amountsIn[0] = amountIn0; diff --git a/test/mocks/TimeLockPenaltyERC20Mock.sol b/test/mocks/TimeLockPenaltyERC20Mock.sol index e8db9fa..7e47417 100644 --- a/test/mocks/TimeLockPenaltyERC20Mock.sol +++ b/test/mocks/TimeLockPenaltyERC20Mock.sol @@ -14,13 +14,7 @@ contract TimeLockPenaltyERC20Mock is TimeLockPenaltyERC20 { uint64 timeLockDuration ) TimeLockPenaltyERC20( - name, - symbol, - underlying, - feeReceiver, - accessManager, - startPenaltyPercentage, - timeLockDuration + name, symbol, underlying, feeReceiver, accessManager, startPenaltyPercentage, timeLockDuration ) { } } diff --git a/tsconfig.json b/tsconfig.json index 7fc6a13..5bfa8c5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,22 @@ { "exclude": ["node_modules"], - "include": ["./hardhat.config.ts", "./scripts", "./deploy", "./test", "./tasks"], + "include": ["./hardhat.config.ts", "./rocketh.ts", "./scripts", "./deploy", "./utils"], "compilerOptions": { - "target": "es2020", - "module": "commonjs", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, + "lib": ["es2023"], + "types": ["node"], + "module": "esnext", + "moduleResolution": "bundler", + "target": "es2022", "strict": true, + "esModuleInterop": true, "skipLibCheck": true, - "resolveJsonModule": true + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "outDir": "dist", + "rootDir": ".", + "paths": { + "@rocketh": ["./rocketh.ts"], + "@generated/*": ["./generated/*"] + } } } diff --git a/utils/getRpcURL.ts b/utils/getRpcURL.ts index aa2f387..2a77467 100644 --- a/utils/getRpcURL.ts +++ b/utils/getRpcURL.ts @@ -20,6 +20,12 @@ export const getRpcURL = (network: string): string => { case "fantom": { return `https://fantom-mainnet.g.alchemy.com/v2/${apiKey}`; } + case "base": { + return `https://base-mainnet.g.alchemy.com/v2/${apiKey}`; + } + case "sonic": { + return `https://sonic-mainnet.g.alchemy.com/v2/${apiKey}`; + } default: { throw new Error(`${network} Network RPC not configured`); } diff --git a/utils/getVerifyConfig.ts b/utils/getVerifyConfig.ts deleted file mode 100644 index 5541052..0000000 --- a/utils/getVerifyConfig.ts +++ /dev/null @@ -1,68 +0,0 @@ -type VerifyConfig = { - etherscan: { - apiUrl: string; - apiKey: string; - }; -}; - -export const getVerifyConfig = (network: string): VerifyConfig => { - switch (network) { - case "mainnet": { - if (!process.env.MAINNET_ETHERSCAN_API_KEY) throw new Error("MAINNET_ETHERSCAN_API_KEY is not set"); - return { - etherscan: { - apiUrl: "https://api.etherscan.io", - apiKey: process.env.MAINNET_ETHERSCAN_API_KEY, - }, - }; - } - case "sepolia": { - if (!process.env.MAINNET_ETHERSCAN_API_KEY) throw new Error("MAINNET_ETHERSCAN_API_KEY is not set"); - return { - etherscan: { - apiUrl: "https://api-sepolia.etherscan.io", - apiKey: process.env.MAINNET_ETHERSCAN_API_KEY, - }, - }; - } - case "polygon": { - if (!process.env.POLYGON_ETHERSCAN_API_KEY) throw new Error("POLYGON_ETHERSCAN_API_KEY is not set"); - return { - etherscan: { - apiUrl: "https://api.polygonscan.com", - apiKey: process.env.POLYGON_ETHERSCAN_API_KEY, - }, - }; - } - case "amoy": { - if (!process.env.POLYGON_ETHERSCAN_API_KEY) throw new Error("POLYGON_ETHERSCAN_API_KEY is not set"); - return { - etherscan: { - apiUrl: "https://api-amoy.polygonscan.com", - apiKey: process.env.POLYGON_ETHERSCAN_API_KEY, - }, - }; - } - case "arbiSepolia": { - if (!process.env.ARBITRUM_ETHERSCAN_API_KEY) throw new Error("ARBITRUM_ETHERSCAN_API_KEY is not set"); - return { - etherscan: { - apiUrl: "https://api-sepolia.arbiscan.io", - apiKey: process.env.ARBITRUM_ETHERSCAN_API_KEY, - }, - }; - } - case "fantom": { - if (!process.env.FANTOM_ETHERSCAN_API_KEY) throw new Error("FANTOM_ETHERSCAN_API_KEY is not set"); - return { - etherscan: { - apiUrl: "https://api.ftmscan.com", - apiKey: process.env.FANTOM_ETHERSCAN_API_KEY, - }, - }; - } - default: { - throw new Error(`${network} Network Verify not configured`); - } - } -};