Skip to content

Commit

Permalink
feat: add pause role, add redemption fee with fee free minter redempt…
Browse files Browse the repository at this point in the history
…ions
  • Loading branch information
jparklev committed Jul 17, 2024
1 parent fe490b7 commit 9d430d5
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 12 deletions.
7 changes: 4 additions & 3 deletions contracts/PToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";

contract PToken is ERC20, AccessControl, Pausable {
bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE");
bytes32 public constant MINT_ROLE = keccak256("MINT_ROLE");
bytes32 public constant BURN_ROLE = keccak256("BURN_ROLE");

constructor(string memory _name, string memory _symbol, uint8 _decimals)
ERC20(_name, _symbol, _decimals)
AccessControl()
{
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(PAUSE_ROLE, msg.sender);
_grantRole(MINT_ROLE, msg.sender);
_grantRole(BURN_ROLE, msg.sender);
}
Expand All @@ -35,11 +36,11 @@ contract PToken is ERC20, AccessControl, Pausable {
return super.transfer(to, amount);
}

function pause() public onlyRole(DEFAULT_ADMIN_ROLE) {
function pause() public onlyRole(PAUSE_ROLE) {
_pause();
}

function unpause() public onlyRole(DEFAULT_ADMIN_ROLE) {
function unpause() public onlyRole(PAUSE_ROLE) {
_unpause();
}
}
81 changes: 74 additions & 7 deletions contracts/PointTokenVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ contract PointTokenVault is UUPSUpgradeable, AccessControlUpgradeable, Multicall
bytes32 public constant REDEMPTION_RIGHTS_PREFIX = keccak256("REDEMPTION_RIGHTS");
bytes32 public constant MERKLE_UPDATER_ROLE = keccak256("MERKLE_UPDATER_ROLE");
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
bytes32 public constant FEE_COLLECTOR = keccak256("FEE_COLLECTOR");

// Deposit asset balances.
mapping(address => mapping(ERC20 => uint256)) public balances; // user => point-earning token => balance
Expand All @@ -43,6 +44,13 @@ contract PointTokenVault is UUPSUpgradeable, AccessControlUpgradeable, Multicall

mapping(address => mapping(address => bool)) public trustedClaimers; // owner => delegate => trustedClaimers

// Fees
uint256 public mintFee;
uint256 public redemptionFee;
mapping(bytes32 => uint256) public pTokenFeeAcc;
mapping(bytes32 => uint256) public rewardTokenFeeAcc;
mapping(address => mapping(bytes32 => uint256)) public feelesslyRedeemedPTokens; // user => pointsId => feelesslyRedeemedPTokens

struct Claim {
bytes32 pointsId;
uint256 totalClaimable;
Expand All @@ -60,13 +68,18 @@ contract PointTokenVault is UUPSUpgradeable, AccessControlUpgradeable, Multicall
event Withdraw(address indexed withdrawer, address indexed receiver, address indexed token, uint256 amount);
event RootUpdated(bytes32 prevRoot, bytes32 newRoot);
event PTokensClaimed(address indexed account, bytes32 indexed pointsId, uint256 amount);
event RewardsClaimed(address indexed owner, address indexed receiver, bytes32 indexed pointsId, uint256 amount);
event RewardsClaimed(
address indexed owner, address indexed receiver, bytes32 indexed pointsId, uint256 amount, uint256 tax
);
event RewardsConverted(address indexed owner, address indexed receiver, bytes32 indexed pointsId, uint256 amount);
event RewardRedemptionSet(
bytes32 indexed pointsId, ERC20 rewardToken, uint256 rewardsPerPToken, bool isMerkleBased
);
event PTokenDeployed(bytes32 indexed pointsId, address indexed pToken);
event CapSet(address indexed token, uint256 prevCap, uint256 cap);
event FeesCollected(bytes32 indexed pointsId, uint256 pTokenFee, uint256 rewardTokenFee);
event MintFeeSet(uint256 mintFee);
event RedemptionFeeSet(uint256 redemptionFee);

error ProofInvalidOrExpired();
error ClaimTooLarge();
Expand Down Expand Up @@ -133,7 +146,10 @@ contract PointTokenVault is UUPSUpgradeable, AccessControlUpgradeable, Multicall
revert NotTrustedClaimer();
}

pTokens[pointsId].mint(_receiver, _claim.amountToClaim);
uint256 pTokenFee = FixedPointMathLib.mulWadUp(_claim.amountToClaim, mintFee);
pTokenFeeAcc[pointsId] += pTokenFee;

pTokens[pointsId].mint(_receiver, _claim.amountToClaim - pTokenFee); // Subtract mint fee.

emit PTokensClaimed(_account, pointsId, _claim.amountToClaim);
}
Expand All @@ -142,7 +158,7 @@ contract PointTokenVault is UUPSUpgradeable, AccessControlUpgradeable, Multicall
trustedClaimers[msg.sender][_account] = _isTrusted;
}

/// @notice Redeems rewards for point tokens
/// @notice Redeems point tokens for rewards
/// @param _claim Details of the claim including the amount and merkle proof
/// @param _receiver The account that will receive the msg.sender redeemed rewards
function redeemRewards(Claim calldata _claim, address _receiver) public {
Expand All @@ -164,10 +180,36 @@ contract PointTokenVault is UUPSUpgradeable, AccessControlUpgradeable, Multicall
_verifyClaimAndUpdateClaimed(_claim, claimHash, msg.sender, claimedRedemptionRights);
}

// Will fail if the user doesn't also have enough point tokens. Assume rewardsPerPToken is 18 decimals.
pTokens[pointsId].burn(msg.sender, FixedPointMathLib.divWadUp(amountToClaim, rewardsPerPToken)); // Round up for burn.
rewardToken.safeTransfer(_receiver, amountToClaim);
emit RewardsClaimed(msg.sender, _receiver, pointsId, amountToClaim);
uint256 pTokensToBurn = FixedPointMathLib.divWadUp(amountToClaim, params.rewardsPerPToken);
pTokens[pointsId].burn(msg.sender, pTokensToBurn);

uint256 claimed = claimedPTokens[msg.sender][pointsId];
uint256 feelesslyRedeemed = feelesslyRedeemedPTokens[msg.sender][pointsId];
uint256 feelesslyRedeemable = claimed - feelesslyRedeemed;

uint256 rewardsToTransfer;
uint256 tax;

if (feelesslyRedeemable >= pTokensToBurn) {
// If all of the pTokens are free to redeem.
rewardsToTransfer = amountToClaim;
feelesslyRedeemedPTokens[msg.sender][pointsId] += pTokensToBurn;
} else {
// If some or all of the pTokens are taxable.
uint256 pTokensToTax = pTokensToBurn - feelesslyRedeemable;
// Taxable pTokens are converted into rewards, and a percentage is taken based on the redemption fee.
tax = FixedPointMathLib.mulWadUp(
FixedPointMathLib.mulWadUp(pTokensToTax, params.rewardsPerPToken), redemptionFee
);
rewardsToTransfer = amountToClaim - tax;
rewardTokenFeeAcc[pointsId] += tax;

feelesslyRedeemedPTokens[msg.sender][pointsId] = claimed;
}

params.rewardToken.safeTransfer(_receiver, rewardsToTransfer);

emit RewardsClaimed(msg.sender, _receiver, pointsId, rewardsToTransfer, tax);
}

/// @notice Mints point tokens for rewards after redemption has been enabled
Expand Down Expand Up @@ -262,6 +304,16 @@ contract PointTokenVault is UUPSUpgradeable, AccessControlUpgradeable, Multicall
emit RewardRedemptionSet(_pointsId, _rewardToken, _rewardsPerPToken, _isMerkleBased);
}

function setMintFee(uint256 _mintFee) external onlyRole(OPERATOR_ROLE) {
mintFee = _mintFee;
emit MintFeeSet(_mintFee);
}

function setRedemptionFee(uint256 _redemptionFee) external onlyRole(OPERATOR_ROLE) {
redemptionFee = _redemptionFee;
emit RedemptionFeeSet(_redemptionFee);
}

function pausePToken(bytes32 _pointsId) external onlyRole(OPERATOR_ROLE) {
pTokens[_pointsId].pause();
}
Expand All @@ -270,6 +322,21 @@ contract PointTokenVault is UUPSUpgradeable, AccessControlUpgradeable, Multicall
pTokens[_pointsId].unpause();
}

function collectFees(bytes32 _pointsId) external onlyRole(FEE_COLLECTOR) {
uint256 pTokenFee = pTokenFeeAcc[_pointsId];
uint256 rewardTokenFee = rewardTokenFeeAcc[_pointsId];

pTokens[_pointsId].mint(msg.sender, pTokenFee);
pTokenFeeAcc[_pointsId] = 0;

if (rewardTokenFee > 0) {
redemptions[_pointsId].rewardToken.safeTransfer(msg.sender, rewardTokenFee);
rewardTokenFeeAcc[_pointsId] = 0;
}

emit FeesCollected(_pointsId, pTokenFee, rewardTokenFee);
}

// To handle arbitrary reward claiming logic.
function execute(address _to, bytes memory _data, uint256 _txGas)
external
Expand Down
126 changes: 124 additions & 2 deletions contracts/test/PointTokenVault.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ contract PointTokenVaultTest is Test {
address admin = makeAddr("admin");
address operator = makeAddr("operator");
address merkleUpdater = makeAddr("merkleUpdater");
address feeCollector = makeAddr("feeCollector");

bytes32 eigenPointsId = LibString.packTwo("Eigen Layer Point", "pEL");

Expand All @@ -42,6 +43,7 @@ contract PointTokenVaultTest is Test {
pointTokenVault.grantRole(pointTokenVault.DEFAULT_ADMIN_ROLE(), admin);
pointTokenVault.grantRole(pointTokenVault.MERKLE_UPDATER_ROLE(), merkleUpdater);
pointTokenVault.grantRole(pointTokenVault.OPERATOR_ROLE(), operator);
pointTokenVault.grantRole(pointTokenVault.FEE_COLLECTOR(), feeCollector);
pointTokenVault.revokeRole(pointTokenVault.DEFAULT_ADMIN_ROLE(), address(this));

// Deploy a mock token
Expand Down Expand Up @@ -403,7 +405,9 @@ contract PointTokenVaultTest is Test {
assertEq(pointTokenVault.pTokens(eigenPointsId).balanceOf(vitalik), 1e18 - 1);
}

event RewardsClaimed(address indexed owner, address indexed receiver, bytes32 indexed pointsId, uint256 amount);
event RewardsClaimed(
address indexed owner, address indexed receiver, bytes32 indexed pointsId, uint256 amount, uint256 tax
);

function test_MerkleBasedRedemption() public {
bytes32 root = 0x409fd0e46d8453765fb513ae35a1899d667478c40233b67360023c86927eb802;
Expand Down Expand Up @@ -440,7 +444,7 @@ contract PointTokenVaultTest is Test {
// Redeem the tokens for rewards with the right proof should succeed
vm.prank(vitalik);
vm.expectEmit(true, true, true, true);
emit RewardsClaimed(vitalik, vitalik, eigenPointsId, 2e18);
emit RewardsClaimed(vitalik, vitalik, eigenPointsId, 2e18, 0);
pointTokenVault.redeemRewards(
PointTokenVault.Claim(eigenPointsId, 2e18, 2e18, validProofVitalikRedemption), vitalik
);
Expand Down Expand Up @@ -560,6 +564,123 @@ contract PointTokenVaultTest is Test {
assertEq(pointTokenVault.pTokens(eigenPointsId).balanceOf(vitalik), 0);
}

event FeesCollected(bytes32 indexed pointsId, uint256 pTokenFee, uint256 rewardTokenFee);

function test_FeeCollectionNoRedemptionFee() public {
bytes32 root = 0x4e40a10ce33f33a4786960a8bb843fe0e170b651acd83da27abc97176c4bed3c;

bytes32[] memory proof = new bytes32[](1);
proof[0] = 0x6d0fcb8de12b1f57f81e49fa18b641487b932cdba4f064409fde3b05d3824ca2;

vm.prank(merkleUpdater);
pointTokenVault.updateRoot(root);

// Set mint fee to 10%
vm.prank(operator);
pointTokenVault.setMintFee(0.1e18); // 10% in WAD

// Claim pTokens
vm.prank(vitalik);
pointTokenVault.claimPTokens(PointTokenVault.Claim(eigenPointsId, 1e18, 1e18, proof), vitalik, vitalik);

assertEq(pointTokenVault.pTokenFeeAcc(eigenPointsId), 0.1e18);
assertEq(pointTokenVault.pTokens(eigenPointsId).balanceOf(vitalik), 0.9e18);

// Set up redemption
rewardToken.mint(address(pointTokenVault), 3e18);
vm.prank(operator);
pointTokenVault.setRedemption(eigenPointsId, rewardToken, 2e18, false);

// Set redemption fee to 5%
vm.prank(operator);
pointTokenVault.setRedemptionFee(0.05e18); // 5% in WAD

// Redeem rewards
bytes32[] memory empty = new bytes32[](0);
vm.prank(vitalik);
pointTokenVault.redeemRewards(PointTokenVault.Claim(eigenPointsId, 1.8e18, 1.8e18, empty), vitalik);

// Collect fees
vm.prank(feeCollector);
vm.expectEmit(true, true, true, true);
emit FeesCollected(eigenPointsId, 0.1e18, 0e18); // No redemption fees
pointTokenVault.collectFees(eigenPointsId);

// Check balances after fee collection
assertEq(pointTokenVault.pTokens(eigenPointsId).balanceOf(feeCollector), 0.1e18);
assertEq(rewardToken.balanceOf(feeCollector), 0);

// Check that fee accumulators are reset
assertEq(pointTokenVault.pTokenFeeAcc(eigenPointsId), 0);
assertEq(pointTokenVault.rewardTokenFeeAcc(eigenPointsId), 0);
}

function test_FeeCollectionRedemptionFee() public {
bytes32 root = 0x4e40a10ce33f33a4786960a8bb843fe0e170b651acd83da27abc97176c4bed3c;

bytes32[] memory proof = new bytes32[](1);
proof[0] = 0x6d0fcb8de12b1f57f81e49fa18b641487b932cdba4f064409fde3b05d3824ca2;

vm.prank(merkleUpdater);
pointTokenVault.updateRoot(root);

// Set mint fee to 10%
vm.prank(operator);
pointTokenVault.setMintFee(0.1e18); // 10% in WAD

// Claim pTokens
vm.prank(vitalik);
pointTokenVault.claimPTokens(PointTokenVault.Claim(eigenPointsId, 1e18, 1e18, proof), vitalik, vitalik);

vm.startPrank(vitalik);
pointTokenVault.pTokens(eigenPointsId).transfer(toly, 0.9e18);
vm.stopPrank();

assertEq(pointTokenVault.pTokenFeeAcc(eigenPointsId), 0.1e18);
assertEq(pointTokenVault.pTokens(eigenPointsId).balanceOf(toly), 0.9e18);

// Set up redemption
rewardToken.mint(address(pointTokenVault), 3e18);
vm.prank(operator);
pointTokenVault.setRedemption(eigenPointsId, rewardToken, 2e18, false);

// Set redemption fee to 5%
vm.prank(operator);
pointTokenVault.setRedemptionFee(0.05e18); // 5% in WAD

// Redeem rewards
bytes32[] memory empty = new bytes32[](0);
vm.prank(toly);
pointTokenVault.redeemRewards(PointTokenVault.Claim(eigenPointsId, 1.8e18, 1.8e18, empty), toly);

// Collect fees
vm.prank(feeCollector);
vm.expectEmit(true, true, true, true);
emit FeesCollected(eigenPointsId, 0.1e18, 0.09e18);
pointTokenVault.collectFees(eigenPointsId);

// Check balances after fee collection
assertEq(pointTokenVault.pTokens(eigenPointsId).balanceOf(feeCollector), 0.1e18);
assertEq(rewardToken.balanceOf(feeCollector), 0.09e18);

// Check that fee accumulators are reset
assertEq(pointTokenVault.pTokenFeeAcc(eigenPointsId), 0);
assertEq(pointTokenVault.rewardTokenFeeAcc(eigenPointsId), 0);
}

function test_FeeCollectionFailsForNonCollector() public {
address nonCollector = address(0x5678);

vm.expectRevert(
abi.encodeWithSelector(
IAccessControl.AccessControlUnauthorizedAccount.selector, nonCollector, pointTokenVault.FEE_COLLECTOR()
)
);

vm.prank(nonCollector);
pointTokenVault.collectFees(eigenPointsId);
}

function test_CantMintPTokensForRewardsMerkleBased() public {
bool IS_MERKLE_BASED = true;

Expand Down Expand Up @@ -708,6 +829,7 @@ contract PointTokenVaultTest is Test {
mockVault.grantRole(pointTokenVault.DEFAULT_ADMIN_ROLE(), admin);
mockVault.grantRole(pointTokenVault.MERKLE_UPDATER_ROLE(), merkleUpdater);
mockVault.grantRole(pointTokenVault.OPERATOR_ROLE(), operator);
mockVault.grantRole(pointTokenVault.FEE_COLLECTOR(), feeCollector);
mockVault.revokeRole(pointTokenVault.DEFAULT_ADMIN_ROLE(), address(this));
}
}
Expand Down

0 comments on commit 9d430d5

Please sign in to comment.