Skip to content

Commit

Permalink
Purchase VT with pufETH (#72)
Browse files Browse the repository at this point in the history
Co-authored-by: ksatyarth2 <ksatyarth2@users.noreply.github.com>
  • Loading branch information
ksatyarth2 and ksatyarth2 authored Nov 4, 2024
1 parent 3be1b03 commit 2b5fc42
Show file tree
Hide file tree
Showing 6 changed files with 649 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

import { Script } from "forge-std/Script.sol";
import { AccessManager } from "@openzeppelin/contracts/access/manager/AccessManager.sol";
import { Multicall } from "@openzeppelin/contracts/utils/Multicall.sol";
import { PUBLIC_ROLE, ROLE_ID_DAO, ROLE_ID_PUFETH_BURNER } from "../../script/Roles.sol";
import { ValidatorTicket } from "../../src/ValidatorTicket.sol";

/**
* @title GenerateValidatorTicketCalldata
* @author Puffer Finance
* @notice Generates the call data to setup the ValidatorTicket contract access
* The returned calldata is queued and executed by the Operations Multisig
* 1. timelock.queueTransaction(address(accessManager), encodedMulticall, 1)
* 2. ... 7 days later ...
* 3. timelock.executeTransaction(address(accessManager), encodedMulticall, 1)
*/
contract GenerateValidatorTicketCalldata is Script {
function run(address validatorTicketProxy) public pure returns (bytes memory) {
bytes[] memory calldatas = new bytes[](3);

// Public functions
bytes4[] memory vtPublicSelectors = new bytes4[](4);
vtPublicSelectors[0] = ValidatorTicket.burn.selector;
vtPublicSelectors[1] = ValidatorTicket.purchaseValidatorTicket.selector;
vtPublicSelectors[2] = ValidatorTicket.purchaseValidatorTicketWithPufETH.selector;
vtPublicSelectors[3] = ValidatorTicket.purchaseValidatorTicketWithPufETHAndPermit.selector;
calldatas[0] = abi.encodeWithSelector(
AccessManager.setTargetFunctionRole.selector, validatorTicketProxy, vtPublicSelectors, PUBLIC_ROLE
);

// DAO-restricted functions
bytes4[] memory vtDaoSelectors = new bytes4[](2);
vtDaoSelectors[0] = ValidatorTicket.setProtocolFeeRate.selector;
vtDaoSelectors[1] = ValidatorTicket.setGuardiansFeeRate.selector;
calldatas[1] = abi.encodeWithSelector(
AccessManager.setTargetFunctionRole.selector, validatorTicketProxy, vtDaoSelectors, ROLE_ID_DAO
);

// Grant PUFETH_BURNER role to ValidatorTicket
calldatas[2] =
abi.encodeWithSelector(AccessManager.grantRole.selector, ROLE_ID_PUFETH_BURNER, validatorTicketProxy, 0);

bytes memory encodedMulticall = abi.encodeCall(Multicall.multicall, (calldatas));
return encodedMulticall;
}
}
64 changes: 64 additions & 0 deletions mainnet-contracts/script/UpgradeValidatorTicket.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

import "forge-std/Script.sol";
import { DeployerHelper } from "./DeployerHelper.s.sol";
import { ValidatorTicket } from "../src/ValidatorTicket.sol";
import { GenerateValidatorTicketCalldata } from "./AccessManagerMigrations/05_GenerateValidatorTicketCalldata.s.sol";
import { IPufferOracle } from "../src/interface/IPufferOracle.sol";
import { AccessManager } from "@openzeppelin/contracts/access/manager/AccessManager.sol";

import { UUPSUpgradeable } from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";

/**
* forge script script/UpgradeValidatorTicket.s.sol:UpgradeValidatorTicket --rpc-url=$RPC_URL --private-key $PK
* add --slow if deploying to a mainnet fork like tenderly
*/
contract UpgradeValidatorTicket is DeployerHelper {
ValidatorTicket public validatorTicket;
bytes public upgradeCallData;
bytes public accessManagerCallData;

function run() public {
GenerateValidatorTicketCalldata calldataGenerator = new GenerateValidatorTicketCalldata();

vm.startBroadcast();

ValidatorTicket validatorTicketImpl = new ValidatorTicket({
guardianModule: payable(address(_getGuardianModule())),
treasury: payable(_getTreasury()),
pufferVault: payable(_getPufferVault()),
pufferOracle: IPufferOracle(address(_getPufferOracle()))
});

validatorTicket = ValidatorTicket(payable(_getValidatorTicket()));

vm.label(address(validatorTicket), "ValidatorTicketProxy");
vm.label(address(validatorTicketImpl), "ValidatorTicketImplementation");

// Upgrade on mainnet
upgradeCallData = abi.encodeCall(UUPSUpgradeable.upgradeToAndCall, (address(validatorTicketImpl), ""));
console.log("Queue TX From Timelock to -> ValidatorTicketProxy", _getValidatorTicket());
console.logBytes(upgradeCallData);
console.log("================================================");
accessManagerCallData = calldataGenerator.run(address(validatorTicket));

console.log("Queue from Timelock -> AccessManager", _getAccessManager());
console.logBytes(accessManagerCallData);

// If on testnet, upgrade and execute access control changes directly
if (block.chainid == holesky) {
// upgrade to implementation
AccessManager(_getAccessManager()).execute(
address(validatorTicket),
abi.encodeCall(UUPSUpgradeable.upgradeToAndCall, (address(validatorTicketImpl), ""))
);

// execute access control changes
(bool success,) = address(_getAccessManager()).call(accessManagerCallData);
console.log("AccessManager.call success", success);
require(success, "AccessManager.call failed");
}
vm.stopBroadcast();
}
}
97 changes: 97 additions & 0 deletions mainnet-contracts/src/ValidatorTicket.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ 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 { Permit } from "./structs/Permit.sol";

/**
* @title ValidatorTicket
Expand Down Expand Up @@ -126,6 +130,42 @@ contract ValidatorTicket is
emit DispersedETH({ treasury: treasuryAmount, guardians: guardiansAmount, vault: vaultAmount });
}

/**
* @inheritdoc IValidatorTicket
* @dev Restricted in this context is like the `whenNotPaused` modifier from Pausable.sol
*/
function purchaseValidatorTicketWithPufETH(address recipient, uint256 vtAmount)
external
virtual
restricted
returns (uint256)
{
return _processPurchaseValidatorTicketWithPufETH(recipient, vtAmount);
}

/**
* @inheritdoc IValidatorTicket
* @dev Restricted in this context is like the `whenNotPaused` modifier from Pausable.sol
*/
function purchaseValidatorTicketWithPufETHAndPermit(address recipient, uint256 vtAmount, Permit calldata permitData)
external
virtual
restricted
returns (uint256)
{
try IERC20Permit(address(PUFFER_VAULT)).permit({
owner: msg.sender,
spender: address(this),
value: permitData.amount,
deadline: permitData.deadline,
v: permitData.v,
r: permitData.r,
s: permitData.s
}) { } catch { }

return _processPurchaseValidatorTicketWithPufETH(recipient, vtAmount);
}

/**
* @notice Burns `amount` from the transaction sender
* @dev Restricted to the PufferProtocol
Expand Down Expand Up @@ -206,4 +246,61 @@ contract ValidatorTicket is
}

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

/**
* @dev Internal function to process the purchase of Validator Tickets with pufETH
* @param recipient The address to receive the minted VTs
* @param vtAmount The amount of Validator Tickets to purchase
* @return pufEthUsed The amount of pufETH used for the purchase
*/
function _processPurchaseValidatorTicketWithPufETH(address recipient, uint256 vtAmount)
internal
returns (uint256 pufEthUsed)
{
require(recipient != address(0), RecipientIsZeroAddress());

uint256 mintPrice = PUFFER_ORACLE.getValidatorTicketPrice();

uint256 requiredETH = vtAmount.mulDiv(mintPrice, 1 ether, Math.Rounding.Ceil);

pufEthUsed = PufferVaultV3(PUFFER_VAULT).convertToSharesUp(requiredETH);

IERC20(PUFFER_VAULT).transferFrom(msg.sender, address(this), pufEthUsed);

_mint(recipient, vtAmount);

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

ValidatorTicket storage $ = _getValidatorTicketStorage();

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

PufferVaultV3(PUFFER_VAULT).burn(burnAmount);

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

return pufEthUsed;
}

/**
* @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);
}
}
}
32 changes: 32 additions & 0 deletions mainnet-contracts/src/interface/IValidatorTicket.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity >=0.8.0 <0.9.0;

import { IPufferOracle } from "../interface/IPufferOracle.sol";
import { Permit } from "../structs/Permit.sol";

/**
* @title IValidatorTicket
Expand All @@ -20,6 +21,11 @@ interface IValidatorTicket {
*/
error InvalidData();

/**
* @dev Thrown when the recipient address is zero
*/
error RecipientIsZeroAddress();

/**
* @notice Emitted when the ETH `amount` in wei is transferred to `to` address
* @dev Signature "0xba7bb5aa419c34d8776b86cc0e9d41e72d74a893a511f361a11af6c05e920c3d"
Expand All @@ -32,6 +38,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 +63,27 @@ 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 vtAmount The amount of Validator Tickets to purchase
* @return pufEthUsed The amount of pufETH used for the purchase
*/
function purchaseValidatorTicketWithPufETH(address recipient, uint256 vtAmount)
external
returns (uint256 pufEthUsed);

/**
* @notice Purchases Validator Tickets with pufETH using permit
* @param recipient The address to receive the minted VTs
* @param vtAmount The amount of Validator Tickets to purchase
* @param permitData The permit data for the pufETH transfer
* @return pufEthUsed The amount of pufETH used for the purchase
*/
function purchaseValidatorTicketWithPufETHAndPermit(address recipient, uint256 vtAmount, Permit calldata permitData)
external
returns (uint256 pufEthUsed);

/**
* @notice Retrieves the current guardians fee rate
* @return The current guardians fee rate
Expand Down
Loading

0 comments on commit 2b5fc42

Please sign in to comment.