Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "@openzeppelin/hardhat-upgrades";
import "dotenv/config";

const PRIVATE_KEY = process.env.PRIVATE_KEY || "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";

const config: HardhatUserConfig = {
solidity: {
version: "0.8.21",
version: "0.8.24",
settings: {
optimizer: {
enabled: true,
Expand Down
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@
"author": "",
"license": "MIT",
"devDependencies": {
"@nomicfoundation/hardhat-toolbox": "^4.0.0",
"@nomicfoundation/hardhat-ethers": "^3.0.0",
"@nomicfoundation/hardhat-chai-matchers": "^2.0.0",
"@nomicfoundation/hardhat-ethers": "^3.0.0",
"@nomicfoundation/hardhat-network-helpers": "^1.0.0",
"@nomicfoundation/hardhat-toolbox": "^4.0.0",
"@nomicfoundation/hardhat-verify": "^2.0.0",
"@typechain/ethers-v6": "^0.5.0",
"@typechain/hardhat": "^9.0.0",
Expand All @@ -45,5 +45,10 @@
"ts-node": "^10.9.0",
"typechain": "^8.3.0",
"typescript": "^5.3.0"
},
"dependencies": {
"@openzeppelin/contracts": "^5.6.1",
"@openzeppelin/contracts-upgradeable": "^5.6.1",
"@openzeppelin/hardhat-upgrades": "^3.9.1"
}
}
47 changes: 47 additions & 0 deletions src/FeeProxyFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {IntuitionFeeProxy} from "./IntuitionFeeProxy.sol";
import {Errors} from "./libraries/Errors.sol";

/// @title FeeProxyFactory
/// @notice Deployer for IntuitionFeeProxy UUPS clones
contract FeeProxyFactory {
/// @notice The absolute implementation address used for all clones
address public immutable proxyImplementation;

/// @notice Emitted when a new proxy is deployed
event ProxyDeployed(address indexed proxyAddress, address indexed owner);

constructor(address _proxyImplementation) {
if (_proxyImplementation == address(0)) {
revert Errors.IntuitionFeeProxy_ZeroAddress();
}
proxyImplementation = _proxyImplementation;
}

/// @notice Deploys a new UUPS Upgradeable proxy and initializes it
/// @param _feeRecipient Address to receive collected fees
/// @param _depositFixedFee Initial fixed fee per deposit (in wei)
/// @param _depositPercentageFee Initial percentage fee for deposits (base 10000)
/// @param _initialAdmins Array of initial admin addresses to whitelist
function deployProxy(
address _feeRecipient,
uint256 _depositFixedFee,
uint256 _depositPercentageFee,
address[] memory _initialAdmins
) external returns (address) {
bytes memory data = abi.encodeWithSelector(
IntuitionFeeProxy.initialize.selector,
_feeRecipient,
_depositFixedFee,
_depositPercentageFee,
_initialAdmins
);

ERC1967Proxy proxy = new ERC1967Proxy(proxyImplementation, data);
emit ProxyDeployed(address(proxy), msg.sender);
return address(proxy);
}
}
72 changes: 47 additions & 25 deletions src/IntuitionFeeProxy.sol
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {IEthMultiVault} from "./interfaces/IEthMultiVault.sol";
import {Errors} from "./libraries/Errors.sol";

/// @title IntuitionFeeProxy
/// @notice Proxy contract for Intuition MultiVault with fee collection
/// @dev Collects fees on deposits and forwards them to a configurable recipient
contract IntuitionFeeProxy {
contract IntuitionFeeProxy is Initializable, UUPSUpgradeable {
// ============ Constants ============

/// @notice Fee denominator for percentage calculations (10000 = 100%)
Expand All @@ -19,6 +21,7 @@ contract IntuitionFeeProxy {
// ============ Immutables ============

/// @notice Reference to the Intuition MultiVault contract
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
IEthMultiVault public immutable ethMultiVault;

// ============ State Variables ============
Expand Down Expand Up @@ -83,32 +86,36 @@ contract IntuitionFeeProxy {
_;
}

// ============ Constructor ============
// ============ Constructor & Initializer ============

/// @notice Initializes the IntuitionFeeProxy contract
/// @param _ethMultiVault Address of the Intuition MultiVault contract
/// @custom:oz-upgrades-unsafe-allow constructor
constructor(address _ethMultiVault) {
if (_ethMultiVault == address(0)) {
revert Errors.IntuitionFeeProxy_InvalidMultiVaultAddress();
}
ethMultiVault = IEthMultiVault(_ethMultiVault);
_disableInitializers();
}

/// @notice Initializes the IntuitionFeeProxy contract logic inside the Proxy
/// @param _feeRecipient Address to receive collected fees
/// @param _depositFixedFee Initial fixed fee per deposit (in wei)
/// @param _depositPercentageFee Initial percentage fee for deposits (base 10000)
/// @param _initialAdmins Array of initial admin addresses to whitelist
constructor(
address _ethMultiVault,
function initialize(
address _feeRecipient,
uint256 _depositFixedFee,
uint256 _depositPercentageFee,
address[] memory _initialAdmins
) {
if (_ethMultiVault == address(0)) {
revert Errors.IntuitionFeeProxy_InvalidMultiVaultAddress();
}
) initializer public {

if (_feeRecipient == address(0)) {
revert Errors.IntuitionFeeProxy_InvalidMultisigAddress();
}
if (_depositPercentageFee > MAX_FEE_PERCENTAGE) {
revert Errors.IntuitionFeeProxy_FeePercentageTooHigh();
}

ethMultiVault = IEthMultiVault(_ethMultiVault);
feeRecipient = _feeRecipient;
depositFixedFee = _depositFixedFee;
depositPercentageFee = _depositPercentageFee;
Expand All @@ -122,6 +129,9 @@ contract IntuitionFeeProxy {
}
}

/// @dev Upgrades the proxy securely, requires admin
function _authorizeUpgrade(address newImplementation) internal override onlyWhitelistedAdmin {}

// ============ Fee Calculation Functions ============

/// @notice Calculate fee for deposits
Expand Down Expand Up @@ -208,6 +218,7 @@ contract IntuitionFeeProxy {
uint256[] calldata assets,
uint256 curveId
) external payable returns (bytes32[] memory atomIds) {
if (receiver != msg.sender) revert Errors.IntuitionFeeProxy_ReceiverMismatch();
if (data.length != assets.length) {
revert Errors.IntuitionFeeProxy_WrongArrayLengths();
}
Expand All @@ -227,7 +238,6 @@ contract IntuitionFeeProxy {
revert Errors.IntuitionFeeProxy_InsufficientValue();
}

_transferFee(fee);
emit FeesCollected(msg.sender, fee, "createAtoms");
emit TransactionForwarded("createAtoms", msg.sender, fee, multiVaultCost, msg.value);

Expand Down Expand Up @@ -271,6 +281,7 @@ contract IntuitionFeeProxy {
uint256[] calldata assets,
uint256 curveId
) external payable returns (bytes32[] memory tripleIds) {
if (receiver != msg.sender) revert Errors.IntuitionFeeProxy_ReceiverMismatch();
if (subjectIds.length != predicateIds.length ||
predicateIds.length != objectIds.length ||
objectIds.length != assets.length) {
Expand All @@ -292,7 +303,6 @@ contract IntuitionFeeProxy {
revert Errors.IntuitionFeeProxy_InsufficientValue();
}

_transferFee(fee);
emit FeesCollected(msg.sender, fee, "createTriples");
emit TransactionForwarded("createTriples", msg.sender, fee, multiVaultCost, msg.value);

Expand Down Expand Up @@ -337,6 +347,7 @@ contract IntuitionFeeProxy {
uint256 curveId,
uint256 minShares
) external payable returns (uint256 shares) {
if (receiver != msg.sender) revert Errors.IntuitionFeeProxy_ReceiverMismatch();
// Must send more than just the fixed fee
if (msg.value <= depositFixedFee) {
revert Errors.IntuitionFeeProxy_InsufficientValue();
Expand All @@ -348,7 +359,6 @@ contract IntuitionFeeProxy {
/ (FEE_DENOMINATOR + depositPercentageFee);
uint256 fee = msg.value - multiVaultAmount;

_transferFee(fee);
emit FeesCollected(msg.sender, fee, "deposit");
emit TransactionForwarded("deposit", msg.sender, fee, multiVaultAmount, msg.value);

Expand Down Expand Up @@ -384,6 +394,7 @@ contract IntuitionFeeProxy {
uint256[] calldata assets,
uint256[] calldata minShares
) external payable returns (uint256[] memory shares) {
if (receiver != msg.sender) revert Errors.IntuitionFeeProxy_ReceiverMismatch();
if (termIds.length != curveIds.length ||
curveIds.length != assets.length ||
assets.length != minShares.length) {
Expand All @@ -399,7 +410,6 @@ contract IntuitionFeeProxy {
revert Errors.IntuitionFeeProxy_InsufficientValue();
}

_transferFee(fee);
emit FeesCollected(msg.sender, fee, "depositBatch");
emit TransactionForwarded("depositBatch", msg.sender, fee, totalDeposit, msg.value);

Expand Down Expand Up @@ -470,19 +480,31 @@ contract IntuitionFeeProxy {
return ethMultiVault.previewDeposit(termId, curveId, assets);
}

// ============ Internal Functions ============
// ============ Admin Withdrawal Functions ============

/// @notice Withdraw accumulated fees
/// @param to Address to receive the funds
/// @param amount Amount to withdraw
function withdrawFees(address to, uint256 amount) external onlyWhitelistedAdmin {
if (to == address(0)) revert Errors.IntuitionFeeProxy_ZeroAddress();
if (amount > address(this).balance) revert Errors.IntuitionFeeProxy_InsufficientValue();

(bool success, ) = to.call{value: amount}("");
if (!success) revert Errors.IntuitionFeeProxy_TransferFailed();
}

/// @notice Transfer collected fees to recipient
/// @param amount Amount to transfer
function _transferFee(uint256 amount) internal {
if (amount > 0) {
(bool success, ) = feeRecipient.call{value: amount}("");
if (!success) {
revert Errors.IntuitionFeeProxy_TransferFailed();
}
}
/// @notice Withdraw all accumulated fees
/// @param to Address to receive the funds
function withdrawAllFees(address to) external onlyWhitelistedAdmin {
if (to == address(0)) revert Errors.IntuitionFeeProxy_ZeroAddress();
uint256 amount = address(this).balance;

(bool success, ) = to.call{value: amount}("");
if (!success) revert Errors.IntuitionFeeProxy_TransferFailed();
}

// ============ Internal Functions ============

/// @notice Sum array of uint256 values
/// @param arr Array to sum
/// @return sum Total sum
Expand Down
3 changes: 3 additions & 0 deletions src/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,7 @@ library Errors {

/// @notice Fee percentage exceeds maximum allowed (100%)
error IntuitionFeeProxy_FeePercentageTooHigh();

/// @notice Receiver does not match msg.sender
error IntuitionFeeProxy_ReceiverMismatch();
}
51 changes: 51 additions & 0 deletions test/FeeProxyFactory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { expect } from "chai";
import { ethers, upgrades } from "hardhat";
import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers";
import { FeeProxyFactory, IntuitionFeeProxy, MockMultiVault } from "../typechain-types";

describe("FeeProxyFactory", function () {
const FEE_RECIPIENT = "0x0000000000000000000000000000000000000001";
const DEPOSIT_FEE = ethers.parseEther("0.1");
const DEPOSIT_PERCENTAGE = 500n;

async function deployFixture() {
const [owner, admin] = await ethers.getSigners();

// Mock Vault
const MockMultiVaultFactory = await ethers.getContractFactory("MockMultiVault");
const mockMultiVault = await MockMultiVaultFactory.deploy();
await mockMultiVault.waitForDeployment();

// Deploy Implementation
const IntuitionFeeProxyFactory = await ethers.getContractFactory("IntuitionFeeProxy");
const implementation = await IntuitionFeeProxyFactory.deploy(await mockMultiVault.getAddress());
await implementation.waitForDeployment();

// Deploy Factory
const FeeProxyFactoryFactory = await ethers.getContractFactory("FeeProxyFactory");
const factory = await FeeProxyFactoryFactory.deploy(await implementation.getAddress());
await factory.waitForDeployment();

return { factory, implementation, mockMultiVault, owner, admin };
}

describe("Deployment", function () {
it("Should deploy new proxy successfully and initialize it", async function () {
const { factory, admin } = await loadFixture(deployFixture);

const tx = await factory.deployProxy(FEE_RECIPIENT, DEPOSIT_FEE, DEPOSIT_PERCENTAGE, [admin.address]);
const receipt = await tx.wait();

const event = receipt?.logs.find((e: any) => e.fragment?.name === "ProxyDeployed");
expect(event).to.not.be.undefined;

const proxyAddress = (event as any).args[0];

const IntuitionFeeProxyFactory = await ethers.getContractFactory("IntuitionFeeProxy");
const clientProxy = IntuitionFeeProxyFactory.attach(proxyAddress) as IntuitionFeeProxy;

expect(await clientProxy.feeRecipient()).to.equal(FEE_RECIPIENT);
expect(await clientProxy.whitelistedAdmins(admin.address)).to.be.true;
});
});
});
Loading