Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

L2 Staking contracts #4

Merged
merged 19 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from 18 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
79 changes: 79 additions & 0 deletions mainnet-contracts/script/DeployPufLocker.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

import "forge-std/Script.sol";
import { AccessManager } from "@openzeppelin/contracts/access/manager/AccessManager.sol";
import { Multicall } from "@openzeppelin/contracts/utils/Multicall.sol";
import { PufLocker } from "../src/PufLocker.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import { ROLE_ID_DAO, PUBLIC_ROLE } from "../script/Roles.sol";

/**
* Check that the simulation
* add --slow if deploying to a mainnet fork like tenderly (its buggy sometimes)
*
* forge script script/DeployPufLocker.s.sol:DeployPufLocker --rpc-url=$RPC_URL --private-key $PK --vvvv
*
* `forge cache clean`
* forge script script/DeployPufLocker.s.sol:DeployPufLocker --rpc-url=$RPC_URL --private-key $PK --broadcast --verify
*/
contract DeployPufLocker is Script {
AccessManager accessManager;

PufLocker pufLockerProxy;

function run() public {
if (block.chainid == 1) {
accessManager = AccessManager(0x8c1686069474410E6243425f4a10177a94EBEE11);
} else if (block.chainid == 17000) {
// Holesky
accessManager = AccessManager(0x180a345906e42293dcAd5CCD9b0e1DB26aE0274e);
} else {
revert("unsupported chain");
}

vm.startBroadcast();

PufLocker pufLockerImpl = new PufLocker();
pufLockerProxy = PufLocker(
address(
new ERC1967Proxy{ salt: bytes32("PufLocker") }(
address(pufLockerImpl), abi.encodeCall(PufLocker.initialize, (address(accessManager)))
)
)
);

console.log("PufLockerProxy:", address(pufLockerProxy));
console.log("PufLocker implementation:", address(pufLockerImpl));

bytes[] memory calldatas = _generateAccessManagerCallData();

if (block.chainid == 1) {
bytes memory multicallData = abi.encodeCall(Multicall.multicall, (calldatas));
console.logBytes(multicallData);
} else if (block.chainid == 17000) {
accessManager.multicall(calldatas);
}
}

function _generateAccessManagerCallData() internal view returns (bytes[] memory) {
bytes[] memory calldatas = new bytes[](2);

bytes4[] memory publicSelectors = new bytes4[](1);
publicSelectors[0] = PufLocker.deposit.selector;

calldatas[0] = abi.encodeWithSelector(
AccessManager.setTargetFunctionRole.selector, address(pufLockerProxy), publicSelectors, PUBLIC_ROLE
);

bytes4[] memory multisigSelectors = new bytes4[](2);
multisigSelectors[0] = PufLocker.setIsAllowedToken.selector;
multisigSelectors[1] = PufLocker.setLockPeriods.selector;

calldatas[1] = abi.encodeWithSelector(
AccessManager.setTargetFunctionRole.selector, address(pufLockerProxy), multisigSelectors, ROLE_ID_DAO
);

return calldatas;
}
}
90 changes: 90 additions & 0 deletions mainnet-contracts/script/DeployPufferL2Depositor.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

import "forge-std/Script.sol";
import { AccessManager } from "@openzeppelin/contracts/access/manager/AccessManager.sol";
import { Multicall } from "@openzeppelin/contracts/utils/Multicall.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import { PufferL2Depositor } from "../src/PufferL2Depositor.sol";
import { PufLocker } from "../src/PufLocker.sol";
import { ROLE_ID_DAO, PUBLIC_ROLE, ROLE_ID_OPERATIONS_MULTISIG } from "../script/Roles.sol";

/**
* Check that the simulation
* add --slow if deploying to a mainnet fork like tenderly (its buggy sometimes)
*
* forge script script/DeployPufferL2Depositor.s.sol:DeployPufferL2Depositor --rpc-url=$RPC_URL --private-key $PK --vvvv
*
* `forge cache clean`
* forge script script/DeployPufferL2Depositor.s.sol:DeployPufferL2Depositor --rpc-url=$RPC_URL --private-key $PK --broadcast --verify
*/
contract DeployPufferL2Depositor is Script {
AccessManager accessManager;

PufferL2Depositor depositor;
PufLocker pufLocker;
address weth;

function run() public {
if (block.chainid == 1) {
accessManager = AccessManager(0x8c1686069474410E6243425f4a10177a94EBEE11);
weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
} else if (block.chainid == 17000) {
// Holesky
accessManager = AccessManager(0x180a345906e42293dcAd5CCD9b0e1DB26aE0274e);
weth = 0x35B1167b4D37931540F4e5189004d1756d1381B0;
} else {
revert("unsupported chain");
}

vm.startBroadcast();

depositor = new PufferL2Depositor(address(accessManager), weth);

address pufLockerImpl = address(new PufLocker());
pufLocker = PufLocker(
address(new ERC1967Proxy(pufLockerImpl, abi.encodeCall(PufLocker.initialize, (address(accessManager)))))
);

bytes[] memory calldatas = new bytes[](4);

bytes4[] memory publicSelectors = new bytes4[](3);
publicSelectors[0] = PufferL2Depositor.deposit.selector;
publicSelectors[1] = PufferL2Depositor.depositETH.selector;
publicSelectors[2] = PufferL2Depositor.revertIfPaused.selector;

calldatas[0] = abi.encodeWithSelector(
AccessManager.setTargetFunctionRole.selector, address(depositor), publicSelectors, PUBLIC_ROLE
);

bytes4[] memory multisigSelectors = new bytes4[](2);
multisigSelectors[0] = PufferL2Depositor.setMigrator.selector;
multisigSelectors[1] = PufferL2Depositor.addNewToken.selector;

calldatas[1] = abi.encodeWithSelector(
AccessManager.setTargetFunctionRole.selector, address(depositor), multisigSelectors, ROLE_ID_DAO
);

bytes4[] memory lockerPublicSelectors = new bytes4[](1);
lockerPublicSelectors[0] = PufLocker.deposit.selector;

calldatas[2] = abi.encodeWithSelector(
AccessManager.setTargetFunctionRole.selector, address(pufLocker), lockerPublicSelectors, PUBLIC_ROLE
);

bytes4[] memory opsLockerSelectors = new bytes4[](2);
opsLockerSelectors[0] = PufLocker.setIsAllowedToken.selector;
opsLockerSelectors[1] = PufLocker.setLockPeriods.selector;

calldatas[3] = abi.encodeWithSelector(
AccessManager.setTargetFunctionRole.selector,
address(pufLocker),
opsLockerSelectors,
ROLE_ID_OPERATIONS_MULTISIG
);

bytes memory multicallData = abi.encodeCall(Multicall.multicall, (calldatas));

console.logBytes(multicallData);
}
}
6 changes: 6 additions & 0 deletions mainnet-contracts/src/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@ error Unauthorized();
* @dev Signature "0xe6c4247b"
*/
error InvalidAddress();

/**
* @notice Thrown when amount is not valid
* @dev Signature "0x2c5211c6"
*/
error InvalidAmount();
185 changes: 185 additions & 0 deletions mainnet-contracts/src/PufLocker.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { AccessManagedUpgradeable } from
"@openzeppelin-contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol";
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { PufLockerStorage } from "./PufLockerStorage.sol";
import { IPufLocker } from "./interface/IPufLocker.sol";
import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { Permit } from "./structs/Permit.sol";
import { InvalidAmount } from "./Errors.sol";

/**
* @title PufLocker
* @author Puffer Finance
* @custom:security-contact security@puffer.fi
*/
contract PufLocker is IPufLocker, AccessManagedUpgradeable, UUPSUpgradeable, PufLockerStorage {
using SafeERC20 for IERC20;

constructor() {
_disableInitializers();
}

function initialize(address accessManager) external initializer {
require(accessManager != address(0));
__AccessManaged_init(accessManager);
}

modifier isAllowedToken(address token) {
PufLockerData storage $ = _getPufLockerStorage();
if (!$.allowedTokens[token]) {
revert TokenNotAllowed();
}
_;
}

/**
* @inheritdoc IPufLocker
* @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol
*/
function deposit(address token, uint128 lockPeriod, Permit calldata permitData)
external
isAllowedToken(token)
restricted
{
if (permitData.amount == 0) {
revert InvalidAmount();
}
PufLockerData storage $ = _getPufLockerStorage();

if (lockPeriod < $.minLockPeriod || lockPeriod > $.maxLockPeriod) {
revert InvalidLockPeriod();
}

// https://docs.openzeppelin.com/contracts/5.x/api/token/erc20#security_considerations
try ERC20Permit(token).permit({
owner: msg.sender,
spender: address(this),
value: permitData.amount,
deadline: permitData.deadline,
v: permitData.v,
s: permitData.s,
r: permitData.r
}) { } catch { }

IERC20(token).safeTransferFrom(msg.sender, address(this), permitData.amount);

uint128 releaseTime = uint128(block.timestamp) + lockPeriod;

$.deposits[msg.sender][token].push(Deposit(uint128(permitData.amount), releaseTime));

emit Deposited(msg.sender, token, uint128(permitData.amount), releaseTime);
}

/**
* @inheritdoc IPufLocker
*/
function withdraw(address token, uint256[] calldata depositIndexes, address recipient) external {
if (recipient == address(0)) {
revert InvalidRecipientAddress();
}

PufLockerData storage $ = _getPufLockerStorage();

uint128 totalAmount = 0;
Deposit[] storage userDeposits = $.deposits[msg.sender][token];

for (uint256 i = 0; i < depositIndexes.length; ++i) {
uint256 index = depositIndexes[i];
if (index >= userDeposits.length) {
revert InvalidDepositIndex();
}

Deposit storage userDeposit = userDeposits[index];
if (userDeposit.releaseTime > uint128(block.timestamp)) {
revert DepositLocked();
}

totalAmount += userDeposit.amount;
userDeposit.amount = 0; // Set amount to zero to mark as withdrawn
}

if (totalAmount == 0) {
revert NoWithdrawableAmount();
}

IERC20(token).safeTransfer(recipient, totalAmount);

emit Withdrawn({ user: msg.sender, token: token, amount: totalAmount, recipient: recipient });
}

/**
* @notice Creates a new staking token contract
* @dev Restricted to Puffer DAO
*/
function setIsAllowedToken(address token, bool allowed) external restricted {
PufLockerData storage $ = _getPufLockerStorage();
$.allowedTokens[token] = allowed;
emit SetTokenIsAllowed(token, allowed);
}

/**
* @notice Creates a new staking token contract
* @dev Restricted to Puffer DAO
*/
function setLockPeriods(uint128 minLock, uint128 maxLock) external restricted {
if (minLock > maxLock) {
revert InvalidLockPeriod();
}
PufLockerData storage $ = _getPufLockerStorage();
emit LockPeriodsChanged($.minLockPeriod, minLock, $.maxLockPeriod, maxLock);
$.minLockPeriod = minLock;
$.maxLockPeriod = maxLock;
}

/**
* @inheritdoc IPufLocker
*/
function getDeposits(address user, address token, uint256 start, uint256 limit)
external
view
returns (Deposit[] memory)
{
PufLockerData storage $ = _getPufLockerStorage();
Deposit[] storage userDeposits = $.deposits[user][token];
uint256 totalDeposits = userDeposits.length;
Deposit[] memory depositPage;

if (start >= totalDeposits) {
return depositPage;
}

uint256 end = start + limit > totalDeposits ? totalDeposits : start + limit;
uint256 count = end - start;

depositPage = new Deposit[](count);
for (uint256 i = 0; i < count; ++i) {
depositPage[i] = userDeposits[start + i];
}

return depositPage;
}

/**
* @inheritdoc IPufLocker
*/
function getAllDeposits(address token, address depositor) external view returns (Deposit[] memory) {
PufLockerData storage $ = _getPufLockerStorage();
return $.deposits[depositor][token];
}

/**
* @inheritdoc IPufLocker
*/
function getLockPeriods() external view returns (uint128, uint128) {
PufLockerData storage $ = _getPufLockerStorage();
return ($.minLockPeriod, $.maxLockPeriod);
}

function _authorizeUpgrade(address newImplementation) internal virtual override restricted { }
}
Loading
Loading