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

feat: Puffer wrapper new contract #98

Merged
merged 13 commits into from
Jan 21, 2025
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
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Parameters
Licensor: Angle Labs, Inc.

Licensed Work: Merkl Smart Contracts
The Licensed Work is (c) 2024 Angle Labs, Inc.
The Licensed Work is (c) 2025 Angle Labs, Inc.

Additional Use Grant: Any uses listed and defined at
merkl-license-grants.angle-labs.eth
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ function run() external broadcast {

The Merkl smart contracts have been audited by Code4rena, find the audit report [here](https://code4rena.com/reports/2023-06-angle).

## Access Control

![Access Control Schema](docs/access_control.svg)

## Media

Don't hesitate to reach out on [Twitter](https://x.com/merkl_xyz) 🐦
75 changes: 75 additions & 0 deletions contracts/partners/tokenWrappers/PointToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.7;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { IAccessControlManager } from "../../interfaces/IAccessControlManager.sol";
import "../../utils/Errors.sol";

/// @title PointToken
/// @author Angle Labs, Inc.
/// @notice Reference contract for points systems within Merkl
contract PointToken is ERC20 {
mapping(address => bool) public minters;
mapping(address => bool) public whitelistedRecipients;
IAccessControlManager public accessControlManager;
uint8 public allowedTransfers;

constructor(
string memory name_,
string memory symbol_,
address _minter,
address _accessControlManager
) ERC20(name_, symbol_) {
if (_accessControlManager == address(0) || _minter == address(0)) revert Errors.ZeroAddress();
accessControlManager = IAccessControlManager(_accessControlManager);
minters[_minter] = true;
}

modifier onlyGovernorOrGuardian() {
if (!accessControlManager.isGovernorOrGuardian(msg.sender)) revert Errors.NotGovernorOrGuardian();
_;
}

modifier onlyMinter() {
if (!minters[msg.sender]) revert Errors.NotTrusted();
_;
}

function mint(address account, uint256 amount) external onlyMinter {
_mint(account, amount);
}

function burn(address account, uint256 amount) external onlyMinter {
_burn(account, amount);
}

function mintBatch(address[] memory accounts, uint256[] memory amounts) external onlyMinter {
uint256 length = accounts.length;
for (uint256 i = 0; i < length; ++i) {
_mint(accounts[i], amounts[i]);
}
}

function toggleMinter(address minter) external onlyGovernorOrGuardian {
minters[minter] = !minters[minter];
}

function toggleAllowedTransfers() external onlyGovernorOrGuardian {
allowedTransfers = 1 - allowedTransfers;
}

function toggleWhitelistedRecipient(address recipient) external onlyGovernorOrGuardian {
whitelistedRecipients[recipient] = !whitelistedRecipients[recipient];
}

function _beforeTokenTransfer(address from, address to, uint256) internal view override {
if (
allowedTransfers == 0 &&
from != address(0) &&
to != address(0) &&
!whitelistedRecipients[from] &&
!whitelistedRecipients[to]
) revert Errors.NotAllowed();
}
}
66 changes: 53 additions & 13 deletions contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s
import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import { IERC20, IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

import { IAccessControlManager } from "../../interfaces/IAccessControlManager.sol";
import { IAccessControlManager } from "./BaseTokenWrapper.sol";

import { UUPSHelper } from "../../utils/UUPSHelper.sol";
import { Errors } from "../../utils/Errors.sol";
Expand Down Expand Up @@ -38,7 +38,7 @@ contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable {
/*//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
VARIABLES
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/
/// @notice `AccessControlManager` contract handling access control
/// @notice `accessControlManager` contract handling access control
IAccessControlManager public accessControlManager;
/// @notice Merkl main functions
address public distributor;
Expand All @@ -53,6 +53,9 @@ contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable {
mapping(address => VestingData) public vestingData;

event Recovered(address indexed token, address indexed to, uint256 amount);
event MerklAddressesUpdated(address indexed _distributionCreator, address indexed _distributor);
event CliffDurationUpdated(uint32 _newCliffDuration);
event FeeRecipientUpdated(address indexed _feeRecipient);

// ================================= FUNCTIONS =================================

Expand All @@ -71,6 +74,7 @@ contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable {
underlying = _underlying;
accessControlManager = _accessControlManager;
cliffDuration = _cliffDuration;
distributionCreator = _distributionCreator;
distributor = IDistributionCreator(_distributionCreator).distributor();
feeRecipient = IDistributionCreator(_distributionCreator).feeRecipient();
}
Expand Down Expand Up @@ -105,15 +109,24 @@ contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable {
if (from == distributor) {
_burn(to, amount);

// Creates a vesting for the `to` address
VestingData storage userVestingData = vestingData[to];
VestingID[] storage userAllVestings = userVestingData.allVestings;
userAllVestings.push(VestingID(uint128(amount), uint128(block.timestamp + cliffDuration)));
uint128 endTimestamp = uint128(block.timestamp + cliffDuration);
if (endTimestamp > block.timestamp) {
// Creates a vesting for the `to` address
VestingData storage userVestingData = vestingData[to];
VestingID[] storage userAllVestings = userVestingData.allVestings;
userAllVestings.push(VestingID(uint128(amount), uint128(block.timestamp + cliffDuration)));
} else {
IERC20(token()).safeTransfer(to, amount);
}
}
}

function claim(address user) external returns (uint256) {
(uint256 claimed, uint256 nextClaimIndex) = _claimable(user);
return claim(user, type(uint256).max);
}

function claim(address user, uint256 maxClaimIndex) public returns (uint256) {
(uint256 claimed, uint256 nextClaimIndex) = _claimable(user, maxClaimIndex);
if (claimed > 0) {
vestingData[user].nextClaimIndex = nextClaimIndex;
IERC20(token()).safeTransfer(user, claimed);
Expand All @@ -122,7 +135,11 @@ contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable {
}

function claimable(address user) external view returns (uint256 amountClaimable) {
(amountClaimable, ) = _claimable(user);
return claimable(user, type(uint256).max);
}

function claimable(address user, uint256 maxClaimIndex) public view returns (uint256 amountClaimable) {
(amountClaimable, ) = _claimable(user, maxClaimIndex);
}

function getUserVestings(
Expand All @@ -133,12 +150,15 @@ contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable {
nextClaimIndex = userVestingData.nextClaimIndex;
}

function _claimable(address user) internal view returns (uint256 amountClaimable, uint256 nextClaimIndex) {
function _claimable(
address user,
uint256 maxClaimIndex
) internal view returns (uint256 amountClaimable, uint256 nextClaimIndex) {
VestingData storage userVestingData = vestingData[user];
VestingID[] storage userAllVestings = userVestingData.allVestings;
uint256 i = userVestingData.nextClaimIndex;
uint256 length = userAllVestings.length;
while (i < length) {
while (i < length && i <= maxClaimIndex) {
VestingID storage userCurrentVesting = userAllVestings[i];
if (block.timestamp > userCurrentVesting.unlockTimestamp) {
amountClaimable += userCurrentVesting.amount;
Expand All @@ -157,7 +177,12 @@ contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable {
_;
}

/// @inheritdoc UUPSHelper
/// @notice Checks whether the `msg.sender` has the governor role or the guardian role
modifier onlyGuardian() {
if (!accessControlManager.isGovernorOrGuardian(msg.sender)) revert Errors.NotGovernorOrGuardian();
_;
}

function _authorizeUpgrade(address) internal view override onlyGovernorUpgrader(accessControlManager) {}

/// @notice Recovers any ERC20 token
Expand All @@ -168,11 +193,26 @@ contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable {
}

function setDistributor(address _distributionCreator) external onlyGovernor {
distributor = IDistributionCreator(_distributionCreator).distributor();
address _distributor = IDistributionCreator(_distributionCreator).distributor();
distributor = _distributor;
distributionCreator = _distributionCreator;
emit MerklAddressesUpdated(_distributionCreator, _distributor);
_setFeeRecipient();
}

function setCliffDuration(uint32 _newCliffDuration) external onlyGuardian {
if (_newCliffDuration < cliffDuration && _newCliffDuration != 0) revert Errors.InvalidParam();
cliffDuration = _newCliffDuration;
emit CliffDurationUpdated(_newCliffDuration);
}

function setFeeRecipient() external {
feeRecipient = IDistributionCreator(distributionCreator).feeRecipient();
_setFeeRecipient();
}

function _setFeeRecipient() internal {
address _feeRecipient = IDistributionCreator(distributionCreator).feeRecipient();
feeRecipient = _feeRecipient;
emit FeeRecipientUpdated(_feeRecipient);
}
}
80 changes: 80 additions & 0 deletions contracts/partners/tokenWrappers/SonicFragment.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.17;

import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

import { IAccessControlManager } from "../../interfaces/IAccessControlManager.sol";
import { Errors } from "../../utils/Errors.sol";

/// @title SonicFragment
/// @notice Contract for Sonic fragments which can be converted upon activation into S tokens
/// @author Angle Labs, Inc.
contract SonicFragment is ERC20 {
using SafeERC20 for IERC20;

/// @notice Contract handling access control
IAccessControlManager public immutable accessControlManager;
/// @notice Address for the S token
address public immutable sToken;

/// @notice Amount of S tokens sent on the contract at the activation of redemption
/// @dev Used to compute the exchange rate between fragments and S tokens
uint128 public sTokenAmount;
/// @notice Total supply of the contract
/// @dev Needs to be stored to compute the exchange rate between fragments and sTokens
uint120 public supply;
/// @notice Whether redemption for S tokens has been activated or not
uint8 public contractSettled;

constructor(
address _accessControlManager,
address recipient,
address _sToken,
uint256 _totalSupply,
string memory _name,
string memory _symbol
) ERC20(_name, _symbol) {
// Zero address check
if (_sToken == address(0)) revert Errors.ZeroAddress();
IAccessControlManager(_accessControlManager).isGovernor(msg.sender);
sToken = _sToken;
accessControlManager = IAccessControlManager(_accessControlManager);
supply = uint120(_totalSupply);
_mint(recipient, _totalSupply);
}

// ================================= MODIFIERS =================================

/// @notice Checks whether the `msg.sender` has the governor role
modifier onlyGovernor() {
if (!accessControlManager.isGovernor(msg.sender)) revert Errors.NotAllowed();
_;
}

/// @notice Activates the contract settlement and enables redemption of fragments into S
/// @dev Can only be called once
function settleContract(uint256 _sTokenAmount) external onlyGovernor {
if (contractSettled > 0) revert Errors.NotAllowed();
contractSettled = 1;
IERC20(sToken).safeTransferFrom(msg.sender, address(this), sTokenAmount);
sTokenAmount = uint128(_sTokenAmount);
}

/// @notice Recovers leftover tokens after sometime
function recover(uint256 amount, address recipient) external onlyGovernor {
IERC20(sToken).safeTransfer(recipient, amount);
sTokenAmount = 0;
}

/// @notice Redeems fragments against S based on a predefined exchange rate
function redeem(uint256 amount, address recipient) external returns (uint256 amountToSend) {
uint128 _sTokenAmount = sTokenAmount;
if (_sTokenAmount == 0) revert Errors.NotAllowed();
_burn(msg.sender, amount);
amountToSend = (amount * _sTokenAmount) / supply;
IERC20(sToken).safeTransfer(recipient, amountToSend);
}
}
Loading
Loading