Skip to content

Commit

Permalink
adds locker contract, deposit caps and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
WalidOfNow committed Jun 14, 2024
1 parent 20c97c7 commit db253b4
Show file tree
Hide file tree
Showing 10 changed files with 562 additions and 30 deletions.
131 changes: 131 additions & 0 deletions mainnet-contracts/src/PufLocker.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.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 { Permit } from "./structs/Permit.sol";

contract PufLocker is AccessManagedUpgradeable, IPufLocker, PufLockerStorage {
function initialize(address accessManager) external initializer {
__AccessManaged_init(accessManager);
}

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

function setAllowedToken(address token, bool allowed) external restricted {
PufLockerData storage $ = _getPufLockerStorage();
$.allowedTokens[token] = allowed;
emit TokenAllowanceChanged(token, allowed);
}

function setLockPeriods(uint40 minLock, uint40 maxLock) external restricted {
if (minLock > maxLock) {
revert InvalidLockPeriod();
}
PufLockerData storage $ = _getPufLockerStorage();
$.minLockPeriod = minLock;
$.maxLockPeriod = maxLock;
}

function deposit(address token, uint40 lockPeriod, Permit calldata permitData) external isAllowedToken(token) {
if (permitData.amount == 0) {
revert InvalidAmount();
}
PufLockerData storage $ = _getPufLockerStorage();
if (lockPeriod < $.minLockPeriod || lockPeriod > $.maxLockPeriod) {
revert InvalidLockPeriod();
}

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

// 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).transferFrom(msg.sender, address(this), permitData.amount);
$.deposits[msg.sender][token].push(Deposit(uint128(permitData.amount), releaseTime));

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

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 > uint40(block.timestamp)) {
revert DepositStillLocked();
}

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

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

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

emit Withdrawn(msg.sender, token, totalAmount, recipient);
}

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

function getLockPeriods() external view returns (uint40, uint40) {
PufLockerData storage $ = _getPufLockerStorage();
return ($.minLockPeriod, $.maxLockPeriod);
}
}
28 changes: 28 additions & 0 deletions mainnet-contracts/src/PufLockerStorage.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

import { IPufLocker } from "./interface/IPufLocker.sol";

/**
* @title PufLockerStorage
* @dev Storage contract for PufLocker to support upgradability
*/
abstract contract PufLockerStorage {
// Storage slot location for PufLocker data
bytes32 private constant _PUF_LOCKER_STORAGE_SLOT =
0xed4b58c94786491f32821dd56ebc03d5f67df2b901c79c3e972343a4fbb3dfed; // keccak256("PufLocker.storage");

struct PufLockerData {
mapping(address => bool) allowedTokens;
mapping(address => mapping(address => IPufLocker.Deposit[])) deposits;
uint40 minLockPeriod;
uint40 maxLockPeriod;
}

function _getPufLockerStorage() internal pure returns (PufLockerData storage $) {
// solhint-disable-next-line no-inline-assembly
assembly {
$.slot := _PUF_LOCKER_STORAGE_SLOT
}
}
}
59 changes: 49 additions & 10 deletions mainnet-contracts/src/PufToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/Sig
import { PufferL2Depositor } from "./PufferL2Depositor.sol";
import { IMigrator } from "./interface/IMigrator.sol";
import { IPufStakingPool } from "./interface/IPufStakingPool.sol";
import { Permit } from "./structs/Permit.sol";

/**
* @title Puf token
Expand Down Expand Up @@ -45,14 +46,17 @@ contract PufToken is IPufStakingPool, ERC20, ERC20Permit {
*/
ERC20 public immutable TOKEN;

constructor(address token, string memory tokenName, string memory tokenSymbol)
uint256 public totalDepositCap;

constructor(address token, string memory tokenName, string memory tokenSymbol, uint256 depositCap)
ERC20(tokenName, tokenSymbol)
ERC20Permit(tokenName)
{
// The Factory is the deployer of the contract
PUFFER_FACTORY = PufferL2Depositor(msg.sender);
TOKEN = ERC20(token);
_TOKEN_DECIMALS = uint256(TOKEN.decimals());
totalDepositCap = depositCap;
}

/**
Expand All @@ -73,6 +77,13 @@ contract PufToken is IPufStakingPool, ERC20, ERC20Permit {
_;
}

modifier onlyPufferFactory() {
if (msg.sender != address(PUFFER_FACTORY)) {
revert Unauthorized();
}
_;
}

/**
* @dev Basic validation of the account and amount
*/
Expand All @@ -86,6 +97,15 @@ contract PufToken is IPufStakingPool, ERC20, ERC20Permit {
_;
}

function depositFrom(address sender, address account, uint256 amount)
external
whenNotPaused
validateAddressAndAmount(account, amount)
onlyPufferFactory
{
_deposit(sender, account, amount);
}

/**
* @notice Deposits the underlying token to receive pufToken to the `account`
*/
Expand All @@ -94,15 +114,7 @@ contract PufToken is IPufStakingPool, ERC20, ERC20Permit {
whenNotPaused
validateAddressAndAmount(account, amount)
{
TOKEN.safeTransferFrom(msg.sender, address(this), amount);

uint256 normalizedAmount = _normalizeAmount(amount);

// Mint puffToken to the account
_mint(account, normalizedAmount);

// Using the original deposit amount in the event
emit Deposited(msg.sender, account, amount);
_deposit(msg.sender, account, amount);
}

/**
Expand Down Expand Up @@ -167,6 +179,33 @@ contract PufToken is IPufStakingPool, ERC20, ERC20Permit {
_migrate({ depositor: depositor, amount: amount, destination: destination, migratorContract: migratorContract });
}

function setDepositCap(uint256 newDepositCap) external onlyPufferFactory {
uint256 deNormalizedTotalSupply = _denormalizeAmount(totalSupply());

if (newDepositCap < deNormalizedTotalSupply) {
revert InvalidAmount();
}

totalDepositCap = newDepositCap;
}

function _deposit(address sender, address account, uint256 amount) internal {
TOKEN.safeTransferFrom(sender, address(this), amount);
uint256 deNormalizedTotalSupply = _denormalizeAmount(totalSupply());

if (deNormalizedTotalSupply + amount > totalDepositCap) {
revert TotalDepositCapReached();
}

uint256 normalizedAmount = _normalizeAmount(amount);

// Mint puffToken to the account
_mint(account, normalizedAmount);

// Using the original deposit amount in the event
emit Deposited(sender, account, amount);
}

/**
* @notice Transfers the Token from this contract using the migrator contract
*/
Expand Down
32 changes: 21 additions & 11 deletions mainnet-contracts/src/PufferL2Depositor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,7 @@ contract PufferL2Depositor is IPufferL2Depositor, AccessManaged {
r: permitData.r
}) { } catch { }

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

_deposit({ token: token, account: account, amount: permitData.amount });
_deposit({ token: token, sender: msg.sender, account: account, amount: permitData.amount });
}

/**
Expand All @@ -72,9 +70,7 @@ contract PufferL2Depositor is IPufferL2Depositor, AccessManaged {
function depositETH(address account) external payable onlySupportedTokens(WETH) restricted {
IWETH(WETH).deposit{ value: msg.value }();

emit DepositedToken(WETH, msg.sender, account, msg.value);

_deposit({ token: WETH, account: account, amount: msg.value });
_deposit({ token: WETH, sender: address(this), account: account, amount: msg.value });
}

/**
Expand All @@ -90,21 +86,34 @@ contract PufferL2Depositor is IPufferL2Depositor, AccessManaged {
* @dev Restricted to Puffer DAO
*/
function setMigrator(address migrator, bool allowed) external restricted {
require(migrator != address(0));
if (migrator == address(0)) {
revert InvalidAccount();
}
isAllowedMigrator[migrator] = allowed;
emit SetIsMigratorAllowed(migrator, allowed);
}

function setDepositCap(address token, uint256 newDepositCap) external onlySupportedTokens(token) restricted {
PufToken pufToken = PufToken(tokens[token]);
uint256 oldDepositCap = pufToken.totalDepositCap();
pufToken.setDepositCap(newDepositCap);

emit DepositCapUpdated(token, oldDepositCap, newDepositCap);
}

/**
* @notice Called by the Token contracts to check if the system is paused
* @dev `restricted` will revert if the system is paused
*/
function revertIfPaused() external restricted { }

function _deposit(address token, address account, uint256 amount) internal {
function _deposit(address token, address sender, address account, uint256 amount) internal {
PufToken pufToken = PufToken(tokens[token]);
IERC20(token).safeIncreaseAllowance(address(pufToken), amount);
pufToken.deposit(account, amount);
if (sender == address(this)) {
IERC20(token).safeIncreaseAllowance(address(pufToken), amount);
}

pufToken.depositFrom(sender, account, amount);

emit DepositedToken(token, msg.sender, account, amount);
}
Expand All @@ -118,7 +127,8 @@ contract PufferL2Depositor is IPufferL2Depositor, AccessManaged {
string memory name = string(abi.encodePacked("puf", ERC20(token).name()));

// Reverts on duplicate token
address pufToken = address(new PufToken{ salt: keccak256(abi.encodePacked(token)) }(token, name, symbol));
address pufToken =
address(new PufToken{ salt: keccak256(abi.encodePacked(token)) }(token, name, symbol, type(uint256).max));

tokens[token] = pufToken;

Expand Down
41 changes: 41 additions & 0 deletions mainnet-contracts/src/interface/IPufLocker.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

import { Permit } from "../structs/Permit.sol";

interface IPufLocker {
// Custom error messages
error NotAdmin();
error TokenNotAllowed();
error InvalidAmount();
error InvalidLockPeriod();
error InvalidDepositIndex();
error DepositStillLocked();
error NoWithdrawableAmount();
error InvalidRecipientAddress();

// Events
event TokenAllowanceChanged(address indexed token, bool allowed);
event Deposited(address indexed user, address indexed token, uint128 amount, uint40 releaseTime);
event Withdrawn(address indexed user, address indexed token, uint128 amount, address recipient);

// Functions
function setAllowedToken(address token, bool allowed) external;

function setLockPeriods(uint40 minLockPeriod, uint40 maxLockPeriod) external;

function deposit(address token, uint40 lockPeriod, Permit calldata permitData) external;

function withdraw(address token, uint256[] calldata depositIndexes, address recipient) external;

function getDeposits(address user, address token, uint256 start, uint256 limit)
external
view
returns (Deposit[] memory);
function getLockPeriods() external view returns (uint40, uint40);

struct Deposit {
uint128 amount;
uint40 releaseTime;
}
}
4 changes: 4 additions & 0 deletions mainnet-contracts/src/interface/IPufStakingPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ interface IPufStakingPool {
error ExpiredSignature();
error InvalidSignature();
error MigratorContractNotAllowed(address migrator);
error Unauthorized();
error TotalDepositCapReached();

event Deposited(address indexed from, address indexed to, uint256 amount);
event Withdrawn(address indexed from, address indexed to, uint256 amount);
Expand All @@ -33,4 +35,6 @@ interface IPufStakingPool {
uint256 signatureExpiry,
bytes memory stakerSignature
) external;

function setDepositCap(uint256 newDepositCap) external;
}
Loading

0 comments on commit db253b4

Please sign in to comment.