From 074ddd92bdf6933cfe538e5be670d3783ae45c4e Mon Sep 17 00:00:00 2001 From: Pablo Veyrat Date: Mon, 2 Dec 2024 10:08:27 +0100 Subject: [PATCH] add: puffer token wrapper --- .../tokenWrappers/PufferPointTokenWrapper.sol | 106 ++++++++++++++++++ scripts/setRewardTokenMinAmount.ts | 6 +- 2 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol diff --git a/contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol b/contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol new file mode 100644 index 0000000..58cc6e8 --- /dev/null +++ b/contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: BUSL-1.1 + +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 { BaseMerklTokenWrapper, IAccessControlManager } from "./BaseTokenWrapper.sol"; + +import "../../utils/Errors.sol"; + +struct VestingID { + uint128 amount; + uint128 unlockTimestamp; +} + +struct VestingData { + VestingID[] allVestings; + uint256 nextClaimIndex; +} + +/// @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 { + using SafeERC20 for IERC20; + + // ================================= CONSTANTS ================================= + + mapping(address => VestingData) public vestingData; + uint256 public cliffDuration; + address public underlying; + + // ================================= FUNCTIONS ================================= + + function initializeWrapper(address _underlying, uint256 _cliffDuration, IAccessControlManager _core) public { + super.initialize(_core); + if (_underlying == address(0)) revert ZeroAddress(); + underlying = _underlying; + cliffDuration = _cliffDuration; + } + + function token() public view override returns (address) { + return underlying; + } + + function _beforeTokenTransfer(address from, address to, uint256 amount) internal override { + // Needs an RDNT approval beforehand, this is how mints of coupons are done + 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); + _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) { + _burn(to, amount); // To avoid having any token aside from on the distributor + } + + if (from == DISTRIBUTOR) { + _burn(to, amount); + _createVesting(to, amount); + } + } + + 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 { + 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 getUserVestings( + address user + ) external view returns (VestingID[] memory allVestings, uint256 nextClaimIndex) { + VestingData storage userVestingData = vestingData[user]; + allVestings = userVestingData.allVestings; + nextClaimIndex = userVestingData.nextClaimIndex; + } +} diff --git a/scripts/setRewardTokenMinAmount.ts b/scripts/setRewardTokenMinAmount.ts index e4b97ee..6a99ba3 100644 --- a/scripts/setRewardTokenMinAmount.ts +++ b/scripts/setRewardTokenMinAmount.ts @@ -24,9 +24,9 @@ async function main() { console.log('Setting reward token min amount'); const token = { - address: '0xA7c167f58833c5e25848837f45A1372491A535eD', - decimals: 6, - minAmount: '1', + address: '0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed', + decimals: 18, + minAmount: '50', } const res = await (