Skip to content

Commit

Permalink
feat: vesting contract
Browse files Browse the repository at this point in the history
  • Loading branch information
sogipec committed Dec 2, 2024
1 parent 43387fe commit fc65005
Show file tree
Hide file tree
Showing 3 changed files with 376 additions and 42 deletions.
146 changes: 109 additions & 37 deletions contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
pragma solidity ^0.8.17;

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

import { BaseMerklTokenWrapper, IAccessControlManager } from "./BaseTokenWrapper.sol";

import "../../utils/UUPSHelper.sol";
import "../../utils/Errors.sol";

struct VestingID {
Expand All @@ -19,81 +21,108 @@ struct VestingData {
uint256 nextClaimIndex;
}

interface IDistributionCreator {
function distributor() external view returns (address);
function feeRecipient() external view returns (address);
}

/// @title PufferPointTokenWrapper
/// @dev This token can only be held by Merkl distributor
/// @dev Transferring to the distributor will require transferring the underlying token to this contract
/// @dev Transferring from the distributor will trigger a vesting action
/// @dev Transferring token to the distributor is permissionless so anyone could mint this wrapper - the only
/// impact would be to forfeit these tokens
contract PufferPointTokenWrapper is BaseMerklTokenWrapper {
contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable {
using SafeERC20 for IERC20;

// ================================= CONSTANTS =================================

mapping(address => VestingData) public vestingData;
uint256 public cliffDuration;
/*//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
VARIABLES
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/
/// @notice `Core` contract handling access control
IAccessControlManager public core;
/// @notice Merkl main functions
address public distributor;
address public feeRecipient;
address public distributionCreator;

/// @notice Underlying token used
address public underlying;
/// @notice Duration of the cliff before which tokens can be claimed
uint32 public cliffDuration;
/// @notice Maps a user address to its vesting data
mapping(address => VestingData) public vestingData;

event Recovered(address indexed token, address indexed to, uint256 amount);

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

function initializeWrapper(address _underlying, uint256 _cliffDuration, IAccessControlManager _core) public {
super.initialize(_core);
if (_underlying == address(0)) revert ZeroAddress();
function initialize(
address _underlying,
uint32 _cliffDuration,
IAccessControlManager _core,
address _distributionCreator
) public initializer {
__ERC20_init(
string.concat("Merkl Token Wrapper - ", IERC20Metadata(_underlying).name()),
string.concat("mtw", IERC20Metadata(_underlying).symbol())
);
__UUPSUpgradeable_init();
if (address(_core) == address(0)) revert ZeroAddress();
underlying = _underlying;
core = _core;
cliffDuration = _cliffDuration;
distributor = IDistributionCreator(_distributionCreator).distributor();
feeRecipient = IDistributionCreator(_distributionCreator).feeRecipient();
}

function isTokenWrapper() external pure returns (bool) {
return true;
}

function token() public view override returns (address) {
function token() public view returns (address) {
return underlying;
}

function _beforeTokenTransfer(address from, address to, uint256 amount) internal override {
// Needs an underlying approval beforehand, this is how mints of wrappers are done
if (to == DISTRIBUTOR) {
if (to == distributor) {
IERC20(underlying).safeTransferFrom(from, address(this), amount);
_mint(from, amount); // These are then transferred to the distributor
}

// Will be burn right after, to avoid having any token aside from on the distributor
if (to == FEE_RECIPIENT) {
IERC20(underlying).safeTransferFrom(from, FEE_RECIPIENT, amount);
// Will be burnt right after, to avoid having any token aside from on the distributor
if (to == feeRecipient) {
IERC20(underlying).safeTransferFrom(from, feeRecipient, amount);
_mint(from, amount); // These are then transferred to the fee manager
}
}

function _afterTokenTransfer(address from, address to, uint256 amount) internal override {
if (to == FEE_RECIPIENT) {
if (to == feeRecipient) {
_burn(to, amount); // To avoid having any token aside from on the distributor
}

if (from == DISTRIBUTOR) {
if (from == distributor) {
_burn(to, amount);
_createVesting(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)));
}
}

function _createVesting(address user, uint256 amount) internal {
VestingData storage userVestingData = vestingData[user];
VestingID[] storage userAllVestings = userVestingData.allVestings;
userAllVestings.push(VestingID(uint128(amount), uint128(block.timestamp + cliffDuration)));
function claim(address user) external returns (uint256) {
(uint256 claimed, uint256 nextClaimIndex) = _claimable(user);
if (claimed > 0) {
vestingData[user].nextClaimIndex = nextClaimIndex;
IERC20(token()).safeTransfer(user, claimed);
}
return claimed;
}

function claim(address user) external {
VestingData storage userVestingData = vestingData[user];
VestingID[] storage userAllVestings = userVestingData.allVestings;
uint256 i = userVestingData.nextClaimIndex;
uint256 claimable;
while (true) {
VestingID storage userCurrentVesting = userAllVestings[i];
if (userCurrentVesting.unlockTimestamp > block.timestamp) {
claimable += userCurrentVesting.amount;
++i;
} else {
userVestingData.nextClaimIndex = i;
break;
}
}
IERC20(token()).safeTransfer(user, claimable);
function claimable(address user) external view returns (uint256 amountClaimable) {
(amountClaimable, ) = _claimable(user);
}

function getUserVestings(
Expand All @@ -103,4 +132,47 @@ contract PufferPointTokenWrapper is BaseMerklTokenWrapper {
allVestings = userVestingData.allVestings;
nextClaimIndex = userVestingData.nextClaimIndex;
}

function _claimable(address user) 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) {
VestingID storage userCurrentVesting = userAllVestings[i];
if (block.timestamp > userCurrentVesting.unlockTimestamp) {
amountClaimable += userCurrentVesting.amount;
nextClaimIndex = ++i;
} else break;
}
}

/*//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ADMIN FUNCTIONS
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/

/// @notice Checks whether the `msg.sender` has the governor role or the guardian role
modifier onlyGovernor() {
if (!core.isGovernor(msg.sender)) revert NotGovernor();
_;
}

/// @inheritdoc UUPSUpgradeable
function _authorizeUpgrade(address) internal view override onlyGovernorUpgrader(core) {}

/// @notice Recovers any ERC20 token
/// @dev Governance only, to trigger only if something went wrong
function recoverERC20(address tokenAddress, address to, uint256 amountToRecover) external onlyGovernor {
IERC20(tokenAddress).safeTransfer(to, amountToRecover);
emit Recovered(tokenAddress, to, amountToRecover);
}

function setDistributor(address _distributionCreator) external onlyGovernor {
distributor = IDistributionCreator(_distributionCreator).distributor();
distributionCreator = _distributionCreator;
}

function setFeeRecipient() external {
feeRecipient = IDistributionCreator(distributionCreator).feeRecipient();
}
}
10 changes: 5 additions & 5 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ const config: HardhatUserConfig = {
rootstock: {
live: true,
url: nodeUrl('rootstock'),
accounts: accounts("rootstock"),
accounts: accounts('rootstock'),
gas: 'auto',
chainId: 30,
verify: {
Expand Down Expand Up @@ -657,13 +657,13 @@ const config: HardhatUserConfig = {
apiKey: {
worldchain: etherscanKey('worldchain'),
},
customChains:[
customChains: [
{
network: 'taiko',
chainId: 167000,
urls: {
apiURL: "https://api.taikoscan.io/api",
browserURL: "https://taikoscan.io/"
apiURL: 'https://api.taikoscan.io/api',
browserURL: 'https://taikoscan.io/',
},
},
{
Expand Down Expand Up @@ -694,7 +694,7 @@ const config: HardhatUserConfig = {
network: 'worldchain',
chainId: 480,
urls: {
apiURL: "https://worldchain-mainnet.explorer.alchemy.com/api",
apiURL: 'https://worldchain-mainnet.explorer.alchemy.com/api',
browserURL: 'https://worldchain-mainnet.explorer.alchemy.com/',
},
},
Expand Down
Loading

0 comments on commit fc65005

Please sign in to comment.