Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions contracts/sPRL/TimeLockPenaltyERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -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
//-------------------------------------------
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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]++;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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
//-------------------------------------------
Expand Down
55 changes: 55 additions & 0 deletions contracts/sPRL/sPRL2Migrator.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Loading