Skip to content

Commit

Permalink
feat: add vt purchase with pufETH functions and basic tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ksatyarth2 committed Oct 25, 2024
1 parent 4703bcc commit 34ac496
Show file tree
Hide file tree
Showing 3 changed files with 262 additions and 1 deletion.
91 changes: 91 additions & 0 deletions mainnet-contracts/src/ValidatorTicket.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { ValidatorTicketStorage } from "./ValidatorTicketStorage.sol";
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import { PufferVaultV3 } from "./PufferVaultV3.sol";
import { IPufferOracle } from "./interface/IPufferOracle.sol";
import { IValidatorTicket } from "./interface/IValidatorTicket.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
import { IPufferVault } from "./interface/IPufferVault.sol";
import { Permit } from "./structs/Permit.sol";

/**
* @title ValidatorTicket
Expand Down Expand Up @@ -206,4 +211,90 @@ contract ValidatorTicket is
}

function _authorizeUpgrade(address newImplementation) internal virtual override restricted { }

/**
* @notice Purchases Validator Tickets with pufETH
* @param recipient The address to receive the minted VTs
* @param pufEthAmount The amount of pufETH to spend
* @return mintedAmount The amount of VTs minted
*/
function purchaseValidatorTicketWithPufETH(address recipient, uint256 pufEthAmount)
external
virtual
restricted
returns (uint256 mintedAmount)
{
return _processPurchaseValidatorTicketWithPufETH(recipient, pufEthAmount);
}

/**
* @notice Purchases Validator Tickets with pufETH using permit
* @param recipient The address to receive the minted VTs
* @param permitData The permit data for the pufETH transfer
* @return mintedAmount The amount of VTs minted
*/
function purchaseValidatorTicketWithPufETHAndPermit(address recipient, Permit calldata permitData)
external
virtual
restricted
returns (uint256 mintedAmount)
{
try IERC20Permit(PUFFER_VAULT).permit(
msg.sender, address(this), permitData.amount, permitData.deadline, permitData.v, permitData.r, permitData.s
) { } catch { }

return _processPurchaseValidatorTicketWithPufETH(recipient, permitData.amount);
}

/**
* @dev Processes the purchase of Validator Tickets with pufETH
* @param recipient The address to receive the minted VTs
* @param pufEthAmount The amount of pufETH to spend
* @return mintedAmount The amount of VTs minted
*/
function _processPurchaseValidatorTicketWithPufETH(address recipient, uint256 pufEthAmount)
private
returns (uint256 mintedAmount)
{
IERC20(PUFFER_VAULT).transferFrom(msg.sender, address(this), pufEthAmount);
ValidatorTicket storage $ = _getValidatorTicketStorage();

uint256 pufETHToETHExchangeRate = PufferVaultV3(PUFFER_VAULT).convertToAssets(1 ether);
uint256 expectedETHAmount = pufEthAmount * pufETHToETHExchangeRate / 1 ether;

uint256 mintPrice = PUFFER_ORACLE.getValidatorTicketPrice();
mintedAmount = (expectedETHAmount * 1 ether) / mintPrice; // * 1 ether is to upscale amount to 18 decimals

_mint(recipient, mintedAmount);

// If we are over the burst threshold, send everything to the treasury
if (PUFFER_ORACLE.isOverBurstThreshold()) {
IERC20(PUFFER_VAULT).transfer(TREASURY, pufEthAmount);
emit DispersedPufETH({ treasury: pufEthAmount, guardians: 0, burned: 0 });
return mintedAmount;
}

uint256 treasuryAmount = _sendPufETH(TREASURY, pufEthAmount, $.protocolFeeRate);
uint256 guardiansAmount = _sendPufETH(GUARDIAN_MODULE, pufEthAmount, $.guardiansFeeRate);
uint256 burnAmount = pufEthAmount - (treasuryAmount + guardiansAmount);

PufferVaultV3(PUFFER_VAULT).burn(burnAmount);

emit DispersedPufETH({ treasury: treasuryAmount, guardians: guardiansAmount, burned: burnAmount });
}

/**
* @dev Calculates the amount of pufETH to send and sends it to the recipient
* @param to The recipient address
* @param amount The total amount of pufETH
* @param rate The fee rate in basis points
* @return toSend The amount of pufETH sent
*/
function _sendPufETH(address to, uint256 amount, uint256 rate) internal virtual returns (uint256 toSend) {
toSend = amount.mulDiv(rate, _BASIS_POINT_SCALE, Math.Rounding.Ceil);

if (toSend != 0) {
IERC20(PUFFER_VAULT).transfer(to, toSend);
}
}
}
13 changes: 13 additions & 0 deletions mainnet-contracts/src/interface/IValidatorTicket.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ interface IValidatorTicket {
*/
event DispersedETH(uint256 treasury, uint256 guardians, uint256 vault);

/**
* @notice Emitted when the pufETH is split between treasury, guardians and the amount burned
*/
event DispersedPufETH(uint256 treasury, uint256 guardians, uint256 burned);

/**
* @notice Emitted when the protocol fee rate is changed
* @dev Signature "0xb51bef650ff5ad43303dbe2e500a74d4fd1bdc9ae05f046bece330e82ae0ba87"
Expand All @@ -52,6 +57,14 @@ interface IValidatorTicket {
*/
function purchaseValidatorTicket(address recipient) external payable returns (uint256);

/**
* @notice Purchases Validator Tickets with pufETH
* @param recipient The address to receive the minted VTs
* @param pufEthAmount The amount of pufETH to spend
* @return mintedAmount The amount of VTs minted
*/
function purchaseValidatorTicketWithPufETH(address recipient, uint256 pufEthAmount) external returns (uint256);

/**
* @notice Retrieves the current guardians fee rate
* @return The current guardians fee rate
Expand Down
159 changes: 158 additions & 1 deletion mainnet-contracts/test/unit/ValidatorTicket.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,24 @@ import { ValidatorTicket } from "../../src/ValidatorTicket.sol";
import { IValidatorTicket } from "../../src/interface/IValidatorTicket.sol";
import { PufferOracle } from "../../src/PufferOracle.sol";
import { PufferOracleV2 } from "../../src/PufferOracleV2.sol";

import { IPufferVault } from "../../src/interface/IPufferVault.sol";
import { PufferVaultV2 } from "../../src/PufferVaultV2.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
import { PUBLIC_ROLE, ROLE_ID_PUFETH_BURNER, ROLE_ID_VAULT_WITHDRAWER } from "../../script/Roles.sol";
import { Permit } from "../../src/structs/Permit.sol";
import "forge-std/console.sol";
/**
* @dev This test is for the ValidatorTicket smart contract with `src/PufferOracle.sol`
*/

contract ValidatorTicketTest is UnitTestHelper {
using ECDSA for bytes32;
using Address for address;
using Address for address payable;

address[] public actors;

function setUp() public override {
// Just call the parent setUp()
super.setUp();
Expand All @@ -27,6 +36,28 @@ contract ValidatorTicketTest is UnitTestHelper {
// In the initial deployment, the PufferOracle will supply that information
pufferOracle = PufferOracleV2(address(new PufferOracle(address(accessManager))));
_skipDefaultFuzzAddresses();
// Grant the ValidatorTicket contract the ROLE_ID_PUFETH_BURNER role
vm.startPrank(_broadcaster);
vm.label(address(validatorTicket), "ValidatorTicket");
console.log("validatorTicket", address(validatorTicket));

bytes4[] memory burnerSelectors = new bytes4[](1);
burnerSelectors[0] = PufferVaultV2.burn.selector;
accessManager.setTargetFunctionRole(address(pufferVault), burnerSelectors, ROLE_ID_PUFETH_BURNER);

bytes4[] memory validatorTicketPublicSelectors = new bytes4[](1);
validatorTicketPublicSelectors[0] = IValidatorTicket.purchaseValidatorTicketWithPufETH.selector;

accessManager.setTargetFunctionRole(address(validatorTicket), validatorTicketPublicSelectors, PUBLIC_ROLE);
accessManager.grantRole(ROLE_ID_PUFETH_BURNER, address(validatorTicket), 0);
vm.stopPrank();

// Initialize actors
actors.push(alice);
actors.push(bob);
actors.push(charlie);
actors.push(dianna);
actors.push(ema);
}

function test_setup() public view {
Expand Down Expand Up @@ -130,4 +161,130 @@ contract ValidatorTicketTest is UnitTestHelper {

assertEq(validatorTicket.getProtocolFeeRate(), newFeeRate, "updated");
}

function test_purchaseValidatorTicketWithPufETH() public {
uint256 pufEthAmount = 10 ether;
address recipient = actors[0];

uint256 exchangeRate = pufferVault.convertToAssets(1 ether);

uint256 vtPrice = pufferOracle.getValidatorTicketPrice();
uint256 expectedMintedAmount = (pufEthAmount * exchangeRate) / vtPrice;

_givePufETH(pufEthAmount, recipient);

vm.startPrank(recipient);

pufferVault.approve(address(validatorTicket), pufEthAmount);

uint256 mintedAmount = validatorTicket.purchaseValidatorTicketWithPufETH(recipient, pufEthAmount);
vm.stopPrank();

assertEq(mintedAmount, expectedMintedAmount, "Minted amount should match expected");
}

function test_purchaseValidatorTicketWithPufETH_exchangeRateChange() public {
uint256 pufEthAmount = 10 ether;
address recipient = actors[1];

uint256 exchangeRate = pufferVault.convertToAssets(1 ether);
assertEq(exchangeRate, 1 ether, "1:1 exchange rate");

_givePufETH(pufEthAmount, recipient);

// Simulate + 10% increase in ETH
deal(address(pufferVault), 1110 ether);
exchangeRate = pufferVault.convertToAssets(1 ether);
assertGt(exchangeRate, 1 ether, "Now exchange rate should be greater than 1");

uint256 vtPrice = pufferOracle.getValidatorTicketPrice();
uint256 expectedMintedAmount = (pufEthAmount * exchangeRate) / vtPrice;

vm.startPrank(recipient);

pufferVault.approve(address(validatorTicket), pufEthAmount);

uint256 mintedAmount = validatorTicket.purchaseValidatorTicketWithPufETH(recipient, pufEthAmount);
vm.stopPrank();

assertEq(mintedAmount, expectedMintedAmount, "Minted amount should match expected");
}

function test_purchaseValidatorTicketWithPufETHAndPermit() public {
uint256 pufEthAmount = 10 ether;
address recipient = actors[2];

uint256 exchangeRate = pufferVault.convertToAssets(1 ether);
uint256 vtPrice = pufferOracle.getValidatorTicketPrice();
uint256 expectedMintedAmount = (pufEthAmount * exchangeRate) / vtPrice;

_givePufETH(pufEthAmount, recipient);

// Create a permit
Permit memory permit = _signPermit(
_testTemps("charlie", address(validatorTicket), pufEthAmount, block.timestamp),
pufferVault.DOMAIN_SEPARATOR()
);

vm.prank(recipient);
uint256 mintedAmount = validatorTicket.purchaseValidatorTicketWithPufETHAndPermit(recipient, permit);

assertEq(mintedAmount, expectedMintedAmount, "Minted amount should match expected");
}

function _givePufETH(uint256 ethAmount, address recipient) internal returns (uint256) {
vm.deal(address(this), ethAmount);

vm.startPrank(address(this));
uint256 pufETHAmount = pufferVault.depositETH{ value: ethAmount }(recipient);
vm.stopPrank();

return pufETHAmount;
}

function _signPermit(bytes32 structHash, bytes32 domainSeparator) internal view returns (Permit memory permit) {
// TODO: Implement signing logic here
permit = Permit({ amount: 10 ether, deadline: block.timestamp + 1 hours, v: 27, r: bytes32(0), s: bytes32(0) });
}

function test_funds_splitting_with_pufETH() public {
uint256 vtPrice = pufferOracle.getValidatorTicketPrice();
uint256 pufEthAmount = vtPrice * 2000; // 2000 VTs worth of pufETH

address recipient = actors[0];
address treasury = validatorTicket.TREASURY();

_givePufETH(pufEthAmount, recipient);

uint256 initialTreasuryBalance = pufferVault.balanceOf(treasury);
uint256 initialGuardianBalance = pufferVault.balanceOf(address(guardianModule));
uint256 initialBurnedAmount = pufferVault.totalSupply();

vm.startPrank(recipient);

pufferVault.approve(address(validatorTicket), pufEthAmount);

uint256 mintedAmount = validatorTicket.purchaseValidatorTicketWithPufETH(recipient, pufEthAmount);
vm.stopPrank();

assertEq(mintedAmount, 2000 ether, "Should mint 2000 VTs");

uint256 expectedTreasuryAmount = (pufEthAmount * 500) / 10000; // 5% to treasury
uint256 expectedGuardianAmount = (pufEthAmount * 50) / 10000; // 0.5% to guardians
uint256 expectedBurnAmount = pufEthAmount - expectedTreasuryAmount - expectedGuardianAmount;

assertEq(
pufferVault.balanceOf(treasury) - initialTreasuryBalance,
expectedTreasuryAmount,
"Treasury should receive 5% of pufETH"
);
assertEq(
pufferVault.balanceOf(address(guardianModule)) - initialGuardianBalance,
expectedGuardianAmount,
"Guardians should receive 0.5% of pufETH"
);
assertEq(
initialBurnedAmount - pufferVault.totalSupply(), expectedBurnAmount, "Remaining pufETH should be burned"
);
}
}

0 comments on commit 34ac496

Please sign in to comment.