Skip to content

Commit

Permalink
feat: Puffer wrapper new contract (#98)
Browse files Browse the repository at this point in the history
* feat: add fragment contract

* rm: hardhat

* fix: puffer wrapper

* fix: bun std

* feat: add sonic fragment

* feat: fragment

* feat: point token

* fix: tests

* fix: access control schema

* fix: wrapper

* fix: deployment

* fix: updating script
  • Loading branch information
sogipec authored Jan 21, 2025
1 parent ceded5b commit ada29f6
Show file tree
Hide file tree
Showing 11 changed files with 575 additions and 17 deletions.
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

0 comments on commit ada29f6

Please sign in to comment.