Skip to content

Commit

Permalink
Merge branch 'fix-review' into chore/collect-fees-after-effects
Browse files Browse the repository at this point in the history
  • Loading branch information
jparklev authored Sep 4, 2024
2 parents 82cc275 + 31f96c5 commit 678ce50
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 41 deletions.
68 changes: 42 additions & 26 deletions contracts/PointTokenVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ contract PointTokenVault is UUPSUpgradeable, AccessControlUpgradeable, Multicall

mapping(address => uint256) public caps; // asset => deposit cap

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

mapping(address => uint256) public totalDeposited; // token => total deposited amount

// Fees
uint256 public mintFee;
Expand All @@ -66,7 +68,7 @@ contract PointTokenVault is UUPSUpgradeable, AccessControlUpgradeable, Multicall

event Deposit(address indexed depositor, address indexed receiver, address indexed token, uint256 amount);
event Withdraw(address indexed withdrawer, address indexed receiver, address indexed token, uint256 amount);
event TrustClaimer(address indexed owner, address indexed delegate, bool isTrusted);
event TrustReceiver(address indexed owner, address indexed delegate, bool isTrusted);
event RootUpdated(bytes32 prevRoot, bytes32 newRoot);
event PTokensClaimed(
address indexed account, address indexed receiver, bytes32 indexed pointsId, uint256 amount, uint256 fee
Expand All @@ -89,13 +91,13 @@ contract PointTokenVault is UUPSUpgradeable, AccessControlUpgradeable, Multicall

error ProofInvalidOrExpired();
error ClaimTooLarge();
error RewardsNotReleased();
error RewardsNotLive();
error CantConvertMerkleRedemption();
error PTokenAlreadyDeployed();
error DepositExceedsCap();
error PTokenNotDeployed();
error AmountTooSmall();
error NotTrustedClaimer();
error NotTrustedReceiver();

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
Expand All @@ -110,25 +112,27 @@ contract PointTokenVault is UUPSUpgradeable, AccessControlUpgradeable, Multicall
_setFeeCollector(_feeCollector);
}

// Rebasing and fee-on-transfer tokens must be wrapped before depositing.
// Rebasing and fee-on-transfer tokens must be wrapped before depositing. ie, they are not supported natively.
function deposit(ERC20 _token, uint256 _amount, address _receiver) public {
uint256 cap = caps[address(_token)];

if (cap != type(uint256).max) {
if (_amount + _token.balanceOf(address(this)) > cap) {
if (totalDeposited[address(_token)] + _amount > cap) {
revert DepositExceedsCap();
}
}

_token.safeTransferFrom(msg.sender, address(this), _amount);

balances[_receiver][_token] += _amount;
totalDeposited[address(_token)] += _amount;

_token.safeTransferFrom(msg.sender, address(this), _amount);

emit Deposit(msg.sender, _receiver, address(_token), _amount);
}

function withdraw(ERC20 _token, uint256 _amount, address _receiver) public {
balances[msg.sender][_token] -= _amount;
totalDeposited[address(_token)] -= _amount;

_token.safeTransfer(_receiver, _amount);

Expand All @@ -149,8 +153,8 @@ contract PointTokenVault is UUPSUpgradeable, AccessControlUpgradeable, Multicall
revert PTokenNotDeployed();
}

if (_account != _receiver && !trustedClaimers[_account][_receiver]) {
revert NotTrustedClaimer();
if (_account != _receiver && !trustedReceivers[_account][_receiver]) {
revert NotTrustedReceiver();
}

uint256 pTokenFee = FixedPointMathLib.mulWadUp(_claim.amountToClaim, mintFee);
Expand All @@ -161,9 +165,9 @@ contract PointTokenVault is UUPSUpgradeable, AccessControlUpgradeable, Multicall
emit PTokensClaimed(_account, _receiver, pointsId, _claim.amountToClaim, pTokenFee);
}

function trustClaimer(address _account, bool _isTrusted) public {
trustedClaimers[msg.sender][_account] = _isTrusted;
emit TrustClaimer(msg.sender, _account, _isTrusted);
function trustReceiver(address _account, bool _isTrusted) public {
trustedReceivers[msg.sender][_account] = _isTrusted;
emit TrustReceiver(msg.sender, _account, _isTrusted);
}

/// @notice Redeems point tokens for rewards
Expand All @@ -177,7 +181,7 @@ contract PointTokenVault is UUPSUpgradeable, AccessControlUpgradeable, Multicall
(params.rewardToken, params.rewardsPerPToken, params.isMerkleBased);

if (address(rewardToken) == address(0)) {
revert RewardsNotReleased();
revert RewardsNotLive();
}

if (isMerkleBased) {
Expand All @@ -188,7 +192,9 @@ contract PointTokenVault is UUPSUpgradeable, AccessControlUpgradeable, Multicall
_verifyClaimAndUpdateClaimed(_claim, claimHash, msg.sender, claimedRedemptionRights);
}

uint256 pTokensToBurn = FixedPointMathLib.divWadUp(amountToClaim, rewardsPerPToken);
uint256 scalingFactor = 10 ** (18 - rewardToken.decimals());
uint256 pTokensToBurn = FixedPointMathLib.divWadUp(amountToClaim * scalingFactor, rewardsPerPToken);

pTokens[pointsId].burn(msg.sender, pTokensToBurn);

uint256 claimed = claimedPTokens[msg.sender][pointsId];
Expand All @@ -205,12 +211,17 @@ contract PointTokenVault is UUPSUpgradeable, AccessControlUpgradeable, Multicall
rewardsToTransfer = amountToClaim;
feelesslyRedeemedPTokens[msg.sender][pointsId] += pTokensToBurn;
} else {
// If some or all of the pTokens need to be charged a fee.
uint256 redeemableWithFee = pTokensToBurn - feelesslyRedeemable;
// fee = amount of pTokens that are not feeless * rewardsPerPToken * redemptionFee
fee = FixedPointMathLib.mulWadUp(
FixedPointMathLib.mulWadUp(redeemableWithFee, rewardsPerPToken), redemptionFee
);
// Calculate the fee. Scope avoids stack too deep errors.
{
// If some or all of the pTokens need to be charged a fee.
uint256 redeemableWithFee = pTokensToBurn - feelesslyRedeemable;
// fee = amount of pTokens that are not feeless * rewardsPerPToken * redemptionFee
fee = FixedPointMathLib.mulWadUp(
FixedPointMathLib.mulWadUp(redeemableWithFee, rewardsPerPToken), redemptionFee
);

fee = fee / scalingFactor; // Downscale to reward token decimals.
}

rewardTokenFeeAcc[pointsId] += fee;
rewardsToTransfer = amountToClaim - fee;
Expand All @@ -232,7 +243,7 @@ contract PointTokenVault is UUPSUpgradeable, AccessControlUpgradeable, Multicall
(params.rewardToken, params.rewardsPerPToken, params.isMerkleBased);

if (address(rewardToken) == address(0)) {
revert RewardsNotReleased();
revert RewardsNotLive();
}

if (isMerkleBased) {
Expand All @@ -241,7 +252,8 @@ contract PointTokenVault is UUPSUpgradeable, AccessControlUpgradeable, Multicall

rewardToken.safeTransferFrom(msg.sender, address(this), _amountToConvert);

uint256 pTokensToMint = FixedPointMathLib.divWadDown(_amountToConvert, rewardsPerPToken); // Round down for mint.
uint256 scalingFactor = 10 ** (18 - rewardToken.decimals());
uint256 pTokensToMint = FixedPointMathLib.divWadDown(_amountToConvert * scalingFactor, rewardsPerPToken);

// Dust guard.
if (pTokensToMint == 0) {
Expand Down Expand Up @@ -349,9 +361,13 @@ contract PointTokenVault is UUPSUpgradeable, AccessControlUpgradeable, Multicall
}

if (rewardTokenFee > 0) {
// There will only be a positive rewardTokenFee if there are reward tokens in this contract available for transfer.
rewardTokenFeeAcc[_pointsId] = 0;
redemptions[_pointsId].rewardToken.safeTransfer(feeCollector, rewardTokenFee);
ERC20 rewardToken = redemptions[_pointsId].rewardToken;
if (address(rewardToken) != address(0)) {
rewardTokenFeeAcc[_pointsId] = 0;
rewardToken.safeTransfer(feeCollector, rewardTokenFee);
} else {
rewardTokenFee = 0; // Do not collect reward token fees if the reward token is unset.
}
}

emit FeesCollected(_pointsId, feeCollector, pTokenFee, rewardTokenFee);
Expand Down
132 changes: 117 additions & 15 deletions contracts/test/PointTokenVault.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,13 @@ import {Test, console} from "forge-std/Test.sol";
import {PointTokenVault} from "../PointTokenVault.sol";
import {PToken} from "../PToken.sol";

import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {ERC1967Utils} from "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol";

import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol";
import {MockERC20, ERC20} from "solmate/test/utils/mocks/MockERC20.sol";

import {LibString} from "solady/utils/LibString.sol";

import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol";

import {PointTokenVaultScripts} from "../script/PointTokenVault.s.sol";

contract PointTokenVaultTest is Test {
Expand Down Expand Up @@ -150,6 +146,47 @@ contract PointTokenVaultTest is Test {
assertEq(pointTokenVault.balances(vitalik, newMockToken), 2e18); // Total 2 tokens deposited
}

function test_DepositCapRewardSameAsDeposit() public {
// Set up an 18 decimal token as both deposit and reward token
MockERC20 token = new MockERC20("Example Token", "EX", 18);

vm.startPrank(operator);
// Set deposit cap for token to 5000
pointTokenVault.setCap(address(token), 5000e18);

// Set token as reward token with 1:1 ratio
pointTokenVault.setRedemption(eigenPointsId, token, 1e18, false);
vm.stopPrank();

// Mint tokens to users
token.mint(vitalik, 5000e18);
token.mint(toly, 2000e18);

// Vitalik deposits 4000 tokens
vm.startPrank(vitalik);
token.approve(address(pointTokenVault), 4000e18);
pointTokenVault.deposit(token, 4000e18, vitalik);
vm.stopPrank();

// Toly converts 2000 tokens to pTokens
vm.startPrank(toly);
token.approve(address(pointTokenVault), 2000e18);
pointTokenVault.convertRewardsToPTokens(toly, eigenPointsId, 2000e18);
vm.stopPrank();

// Assert current token balance in vault
assertEq(token.balanceOf(address(pointTokenVault)), 6000e18);

// Try to deposit 1000 tokens, which should succeed
vm.startPrank(vitalik);
token.approve(address(pointTokenVault), 1000e18);
pointTokenVault.deposit(token, 1000e18, vitalik);
vm.stopPrank();

// Assert that 5000 tokens have been deposited
assertEq(pointTokenVault.balances(vitalik, token), 5000e18);
}

function test_DeployPToken() public {
// Can't deploy the same token twice
vm.expectRevert(PointTokenVault.PTokenAlreadyDeployed.selector);
Expand Down Expand Up @@ -409,6 +446,31 @@ contract PointTokenVaultTest is Test {
assertEq(pointTokenVault.pTokens(eigenPointsId).balanceOf(vitalik), 1e18 - 1);
}

function test_RedeemRewardsWith6DecimalToken() public {
// Setup a mock 6-decimal token (like USDC)
MockERC20 usdcReward = new MockERC20("USDC Reward", "USDC", 6);

// Mint 1,000,000 USDC to the vault
usdcReward.mint(address(pointTokenVault), 1_000_000 * 1e6);

// Set redemption parameters (1 pToken = 1 USDC)
vm.prank(operator);
pointTokenVault.setRedemption(eigenPointsId, usdcReward, 1e18, false);

// Mint 1 pToken to vitalik
vm.startPrank(address(pointTokenVault));
pointTokenVault.pTokens(eigenPointsId).mint(vitalik, 1e18);
vm.stopPrank();

// Vitalik redeems 1 pToken for 1 USDC
vm.prank(vitalik);
pointTokenVault.redeemRewards(PointTokenVault.Claim(eigenPointsId, 1e6, 1e6, new bytes32[](0)), vitalik);

// Check balances
assertEq(usdcReward.balanceOf(vitalik), 1e6, "Vitalik should receive 1 USDC");
assertEq(pointTokenVault.pTokens(eigenPointsId).balanceOf(vitalik), 0, "Vitalik should have 0 pTokens left");
}

event RewardsClaimed(
address indexed owner, address indexed receiver, bytes32 indexed pointsId, uint256 amount, uint256 tax
);
Expand Down Expand Up @@ -482,7 +544,7 @@ contract PointTokenVaultTest is Test {
assertEq(pointTokenVault.pTokens(eigenPointsId).balanceOf(vitalik), 1e18);
}

function test_TrustedClaimer() public {
function test_TrustedReceiver() public {
bytes32 root = 0x4e40a10ce33f33a4786960a8bb843fe0e170b651acd83da27abc97176c4bed3c;

bytes32[] memory proof = new bytes32[](1);
Expand All @@ -493,12 +555,12 @@ contract PointTokenVaultTest is Test {

// Toly tries to claim vitalik's pTokens (should fail)
vm.prank(toly);
vm.expectRevert(PointTokenVault.NotTrustedClaimer.selector);
vm.expectRevert(PointTokenVault.NotTrustedReceiver.selector);
pointTokenVault.claimPTokens(PointTokenVault.Claim(eigenPointsId, 1e18, 0.6e18, proof), vitalik, toly);

// Vitalik delegates claiming rights to Toly
vm.prank(vitalik);
pointTokenVault.trustClaimer(toly, true);
pointTokenVault.trustReceiver(toly, true);

// Toly claims the half of Vitalik's pTokens
vm.prank(toly);
Expand All @@ -519,7 +581,7 @@ contract PointTokenVaultTest is Test {

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

function test_MintPTokensForRewards() public {
function test_ConvertRewardsToPTokens() public {
bytes32 root = 0x4e40a10ce33f33a4786960a8bb843fe0e170b651acd83da27abc97176c4bed3c;

bytes32[] memory proof = new bytes32[](1);
Expand All @@ -535,9 +597,9 @@ contract PointTokenVaultTest is Test {

// Cannot redeem pTokens or convert rewards before redemption data is set
bytes32[] memory empty = new bytes32[](0);
vm.expectRevert(PointTokenVault.RewardsNotReleased.selector);
vm.expectRevert(PointTokenVault.RewardsNotLive.selector);
pointTokenVault.redeemRewards(PointTokenVault.Claim(eigenPointsId, 2e18, 2e18, empty), vitalik);
vm.expectRevert(PointTokenVault.RewardsNotReleased.selector);
vm.expectRevert(PointTokenVault.RewardsNotLive.selector);
pointTokenVault.convertRewardsToPTokens(vitalik, eigenPointsId, 1e18);

vm.prank(operator);
Expand Down Expand Up @@ -568,17 +630,43 @@ contract PointTokenVaultTest is Test {
assertEq(pointTokenVault.pTokens(eigenPointsId).balanceOf(vitalik), 0);
}

function test_ConvertRewardsToPTokensWith6DecimalToken() public {
// Setup a mock 6-decimal token (like USDC)
MockERC20 usdcReward = new MockERC20("USDC Reward", "USDC", 6);

// Mint 1,000,000 USDC to vitalik
usdcReward.mint(vitalik, 1_000_000 * 1e6);

// Set redemption parameters (1 pToken = 1 USDC)
vm.prank(operator);
pointTokenVault.setRedemption(eigenPointsId, usdcReward, 1e18, false);

// Approve USDC spend
vm.prank(vitalik);
usdcReward.approve(address(pointTokenVault), type(uint256).max);

// Vitalik converts 1 USDC to 1 pToken
vm.prank(vitalik);
pointTokenVault.convertRewardsToPTokens(vitalik, eigenPointsId, 1e6);

// Check balances
assertEq(usdcReward.balanceOf(vitalik), 999_999 * 1e6, "Vitalik should have 999,999 USDC left");
assertEq(pointTokenVault.pTokens(eigenPointsId).balanceOf(vitalik), 1e18, "Vitalik should receive 1 pToken");
}

event FeeCollectorSet(address feeCollector);

function test_setFeeCollector() public {
vm.prank(admin);
vm.expectEmit(true,true,true,true);
vm.expectEmit(true, true, true, true);
emit FeeCollectorSet(toly);
pointTokenVault.setFeeCollector(toly);

vm.expectRevert(
abi.encodeWithSelector(
IAccessControl.AccessControlUnauthorizedAccount.selector, address(vitalik), pointTokenVault.DEFAULT_ADMIN_ROLE()
IAccessControl.AccessControlUnauthorizedAccount.selector,
address(vitalik),
pointTokenVault.DEFAULT_ADMIN_ROLE()
)
);
vm.prank(vitalik);
Expand Down Expand Up @@ -675,9 +763,23 @@ contract PointTokenVaultTest is Test {
vm.prank(toly);
pointTokenVault.redeemRewards(PointTokenVault.Claim(eigenPointsId, 1.8e18, 1.8e18, empty), toly);

// Unset redemption
vm.prank(operator);
pointTokenVault.setRedemption(eigenPointsId, ERC20(address(0)), 0, false);

// No reward token fees are collected.
vm.expectEmit(true, true, true, true);
emit FeesCollected(eigenPointsId, pointTokenVault.feeCollector(), 0.1e18, 0);
pointTokenVault.collectFees(eigenPointsId);
assertEq(rewardToken.balanceOf(pointTokenVault.feeCollector()), 0);

// Set redemption again
vm.prank(operator);
pointTokenVault.setRedemption(eigenPointsId, rewardToken, 2e18, false);

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

// Check balances after fee collection
Expand Down

0 comments on commit 678ce50

Please sign in to comment.