diff --git a/evm/modules/01_Market.ts b/evm/modules/01_Market.ts index 5c1d0e17..04f90b03 100644 --- a/evm/modules/01_Market.ts +++ b/evm/modules/01_Market.ts @@ -84,7 +84,7 @@ export default buildModule('Market', (m) => { pexfi, universalRouter, m.getParameter('weth_address'), - [zeroAddress, pexfi, 10000, 60, zeroAddress], + [zeroAddress, pexfi, 10000, 200, zeroAddress], ]) m.call(Market, 'changeImplementationAddress', [bytes32('FeeCollector'), feeCollector], { id: 'regFeeCollector', @@ -129,9 +129,9 @@ export default buildModule('Market', (m) => { id: 'collateralWhitelist', }) m.call(CollateralWhitelist, 'addToWhitelist', [pexfiVault]) - // With OOv3 defaults (50% burn), Minimum Bond = Final Fee * 2 = 200,000 tokens. + // With OOv3 defaults (50% burn), Minimum Bond = Final Fee * 2 = 200 tokens. // This ensures only the deployer can assert/dispute initially. - const initialFinalFee = 100_000n * 10n ** 18n + const initialFinalFee = 100n * 10n ** 18n m.call(Store, 'setFinalFee', [pexfiVault, { rawValue: initialFinalFee }]) // Placeholder Oracle address — syncUmaParams requires it at deploy time. diff --git a/evm/modules/03_BuyPexfi.ts b/evm/modules/03_BuyPexfi.ts new file mode 100644 index 00000000..77c974aa --- /dev/null +++ b/evm/modules/03_BuyPexfi.ts @@ -0,0 +1,23 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' +import { parseEther } from 'viem' + +/** + * This module performs a buyback of PEXFI tokens from the Uniswap v4 pool. + * It uses a standalone action contract to perform the swap in a single transaction. + */ +export default buildModule('BuyPexfi', (m) => { + const amount = parseEther('0.0003') + const pexfi = m.getParameter('pexfi_address', '0xCCA91e36E5c15163C5258832C57774072Db257C5') + const universalRouter = m.getParameter('uniswapUniversalRouter') + + // Deploy the action contract + const action = m.contract('PexfiBuybackAction') + + // Execute the buyback + m.call(action, 'run', [universalRouter, pexfi], { + id: 'buybackPexfi', + value: amount, + }) + + return { action } +}) diff --git a/evm/protocol/PexfiBuybackAction.sol b/evm/protocol/PexfiBuybackAction.sol new file mode 100644 index 00000000..8f592d93 --- /dev/null +++ b/evm/protocol/PexfiBuybackAction.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {IUniversalRouter} from "@uniswap/universal-router/contracts/interfaces/IUniversalRouter.sol"; +import {Commands} from "@uniswap/universal-router/contracts/libraries/Commands.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @title PexfiBuybackAction + * @notice Standalone contract to buy PEXFI from Uniswap v4 pool in a single transaction. + */ +contract PexfiBuybackAction { + function run(address router, address pexfi) external payable { + require(msg.value > 0, "No ETH sent"); + + PoolKey memory poolKey = PoolKey({ + currency0: Currency.wrap(address(0)), + currency1: Currency.wrap(pexfi), + fee: 10000, + tickSpacing: 200, + hooks: IHooks(address(0)) + }); + + bytes memory v4Actions = abi.encodePacked( + uint8(Actions.SWAP_EXACT_IN_SINGLE), + uint8(Actions.SETTLE), + uint8(Actions.TAKE_ALL) + ); + + bytes[] memory v4Inputs = new bytes[](3); + + // 1. Swap ETH for PEXFI + v4Inputs[0] = abi.encode( + poolKey, + true, // zeroForOne (ETH is currency0) + uint128(msg.value), + uint128(0), // amountOutMinimum + "" // hookData + ); + + // 2. Settle ETH (pay from the ETH sent to this contract) + v4Inputs[1] = abi.encode( + Currency.wrap(address(0)), + msg.value, + false // payerIsUser = false (take from this contract) + ); + + // 3. Take PEXFI (send to this contract) + v4Inputs[2] = abi.encode( + Currency.wrap(pexfi), + 0 // minAmount + ); + + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.V4_SWAP))); + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(v4Actions, v4Inputs); + + // Execute the swap on Universal Router + IUniversalRouter(router).execute{value: msg.value}(commands, inputs, block.timestamp + 600); + + // Send bought PEXFI tokens back to the caller + uint256 pexfiBal = IERC20(pexfi).balanceOf(address(this)); + if (pexfiBal > 0) { + bool success = IERC20(pexfi).transfer(msg.sender, pexfiBal); + require(success, "Transfer failed"); + } + + // Send back any remaining ETH + if (address(this).balance > 0) { + (bool success, ) = msg.sender.call{value: address(this).balance}(""); + require(success, "ETH refund failed"); + } + } + + receive() external payable {} +} diff --git a/package-lock.json b/package-lock.json index 56cc883d..79558448 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "@nomicfoundation/hardhat-verify": "^3.0.9", "@nomicfoundation/hardhat-viem": "^3.0.2", "@nomicfoundation/hardhat-viem-assertions": "^3.0.5", + "@openzeppelin/contracts": "^5.4.0", "@openzeppelin/contracts-upgradeable": "^5.4.0", "@playwright/test": "^1.48.2", "@testing-library/dom": "^10.4.1", @@ -8497,6 +8498,7 @@ "version": "5.6.1", "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.6.1.tgz", "integrity": "sha512-Ly6SlsVJ3mj+b18W3R8gNufB7dTICT105fJhodGAGgyC2oqnBAhqSiNDJ8V8DLY05cCz81GLI0CU5vNYA1EC/w==", + "dev": true, "license": "MIT" }, "node_modules/@openzeppelin/contracts-upgradeable": { diff --git a/tests/hardhat/ignition/03_BuyPexfi.test.ts b/tests/hardhat/ignition/03_BuyPexfi.test.ts new file mode 100644 index 00000000..8bd268ad --- /dev/null +++ b/tests/hardhat/ignition/03_BuyPexfi.test.ts @@ -0,0 +1,22 @@ +import { describe, test } from 'node:test' +import * as assert from 'node:assert' +import BuyPexfiModule from '@evm/modules/03_BuyPexfi.ts' +import deploy from './01_Market.test.ts' + +describe('BuyPexfi Module', () => { + test('should execute buyback successfully', async () => { + const { ignition, universalRouter, pexfi } = await deploy() + + const parameters = { + BuyPexfi: { + pexfi_address: pexfi.address, + uniswapUniversalRouter: universalRouter.address, + }, + } + + // Deploy and run the BuyPexfi module + const { action } = await ignition.deploy(BuyPexfiModule, { parameters }) + + assert.ok(action.address, 'PexfiBuybackAction should be deployed') + }) +})