diff --git a/src/IntuitionFeeProxy.sol b/src/IntuitionFeeProxy.sol index 35993d7..12a71fa 100644 --- a/src/IntuitionFeeProxy.sol +++ b/src/IntuitionFeeProxy.sol @@ -195,9 +195,8 @@ contract IntuitionFeeProxy { // ============ Proxy Functions (Payable) ============ - /// @notice Create atoms with fee collection and deposit to receiver - /// @dev Receiver must have approved this proxy on MultiVault for DEPOSIT - /// @param receiver Address to receive the shares (the real user) + /// @notice Create atoms with fee collection and deposit to the caller + /// @param receiver Address to receive the shares, which must match msg.sender /// @param data Array of atom data (IPFS URIs as bytes) /// @param assets Array of deposit amounts for each atom (on top of creation cost) /// @param curveId Bonding curve ID for deposits (1 = linear, 2 = progressive) @@ -211,6 +210,7 @@ contract IntuitionFeeProxy { if (data.length != assets.length) { revert Errors.IntuitionFeeProxy_WrongArrayLengths(); } + _validateReceiver(receiver); uint256 count = data.length; uint256 atomCost = ethMultiVault.getAtomCost(); @@ -254,9 +254,8 @@ contract IntuitionFeeProxy { return atomIds; } - /// @notice Create triples with fee collection and deposit to receiver - /// @dev Receiver must have approved this proxy on MultiVault for DEPOSIT - /// @param receiver Address to receive the shares (the real user) + /// @notice Create triples with fee collection and deposit to the caller + /// @param receiver Address to receive the shares, which must match msg.sender /// @param subjectIds Array of subject atom IDs /// @param predicateIds Array of predicate atom IDs /// @param objectIds Array of object atom IDs @@ -276,6 +275,7 @@ contract IntuitionFeeProxy { objectIds.length != assets.length) { revert Errors.IntuitionFeeProxy_WrongArrayLengths(); } + _validateReceiver(receiver); uint256 count = subjectIds.length; uint256 tripleCost = ethMultiVault.getTripleCost(); @@ -326,7 +326,7 @@ contract IntuitionFeeProxy { /// @notice Deposit with fee collection - SAME SIGNATURE AS MULTIVAULT /// @dev Fee is calculated from msg.value using inverse formula - /// @param receiver Address to receive shares + /// @param receiver Address to receive shares, which must match msg.sender /// @param termId Vault ID (atom or triple) /// @param curveId Bonding curve ID /// @param minShares Minimum shares expected @@ -337,6 +337,8 @@ contract IntuitionFeeProxy { uint256 curveId, uint256 minShares ) external payable returns (uint256 shares) { + _validateReceiver(receiver); + // Must send more than just the fixed fee if (msg.value <= depositFixedFee) { revert Errors.IntuitionFeeProxy_InsufficientValue(); @@ -371,7 +373,7 @@ contract IntuitionFeeProxy { } /// @notice Batch deposit with fee collection - /// @param receiver Address to receive shares + /// @param receiver Address to receive shares, which must match msg.sender /// @param termIds Array of vault IDs /// @param curveIds Array of curve IDs /// @param assets Array of deposit amounts @@ -389,6 +391,7 @@ contract IntuitionFeeProxy { assets.length != minShares.length) { revert Errors.IntuitionFeeProxy_WrongArrayLengths(); } + _validateReceiver(receiver); uint256 totalDeposit = _sumArray(assets); // Fee: fixed fee per deposit + percentage of total @@ -472,6 +475,14 @@ contract IntuitionFeeProxy { // ============ Internal Functions ============ + /// @notice Ensure proxy deposits cannot be redirected to another receiver + /// @param receiver Address requested as the MultiVault receiver + function _validateReceiver(address receiver) internal view { + if (receiver != msg.sender) { + revert Errors.IntuitionFeeProxy_ReceiverMismatch(); + } + } + /// @notice Transfer collected fees to recipient /// @param amount Amount to transfer function _transferFee(uint256 amount) internal { diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 839d842..b3a06c8 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -25,6 +25,9 @@ library Errors { /// @notice Zero address provided where not allowed error IntuitionFeeProxy_ZeroAddress(); + /// @notice Receiver address does not match the transaction sender + error IntuitionFeeProxy_ReceiverMismatch(); + /// @notice Fee percentage exceeds maximum allowed (100%) error IntuitionFeeProxy_FeePercentageTooHigh(); } diff --git a/test/IntuitionFeeProxy.test.ts b/test/IntuitionFeeProxy.test.ts index 9365c5e..415bd63 100644 --- a/test/IntuitionFeeProxy.test.ts +++ b/test/IntuitionFeeProxy.test.ts @@ -266,6 +266,23 @@ describe("IntuitionFeeProxy", function () { proxy.connect(user).createAtoms(user.address, data, assets, curveId, { value: ethers.parseEther("0.01") }) ).to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_InsufficientValue"); }); + + it("Should revert when createAtoms receiver is not the caller", async function () { + const { proxy, mockMultiVault, user, nonAdmin } = await loadFixture(deployFixture); + + const data = [ethers.toUtf8Bytes("ipfs://atom1")]; + const assets = [ethers.parseEther("0.01")]; + const curveId = 1n; + + const atomCost = await mockMultiVault.getAtomCost(); + const totalDeposit = ethers.parseEther("0.01"); + const fee = await proxy.calculateDepositFee(1n, totalDeposit); + const totalRequired = atomCost + totalDeposit + fee; + + await expect( + proxy.connect(user).createAtoms(nonAdmin.address, data, assets, curveId, { value: totalRequired }) + ).to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_ReceiverMismatch"); + }); }); describe("Proxy Functions - createTriples", function () { @@ -308,6 +325,25 @@ describe("IntuitionFeeProxy", function () { proxy.connect(user).createTriples(user.address, subjectIds, predicateIds, objectIds, assets, curveId, { value: ethers.parseEther("10") }) ).to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_WrongArrayLengths"); }); + + it("Should revert when createTriples receiver is not the caller", async function () { + const { proxy, mockMultiVault, user, nonAdmin } = await loadFixture(deployFixture); + + const subjectIds = [ethers.zeroPadValue("0x01", 32)]; + const predicateIds = [ethers.zeroPadValue("0x02", 32)]; + const objectIds = [ethers.zeroPadValue("0x03", 32)]; + const assets = [ethers.parseEther("0.01")]; + const curveId = 1n; + + const tripleCost = await mockMultiVault.getTripleCost(); + const totalDeposit = ethers.parseEther("0.01"); + const fee = await proxy.calculateDepositFee(1n, totalDeposit); + const totalRequired = tripleCost + totalDeposit + fee; + + await expect( + proxy.connect(user).createTriples(nonAdmin.address, subjectIds, predicateIds, objectIds, assets, curveId, { value: totalRequired }) + ).to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_ReceiverMismatch"); + }); }); describe("Proxy Functions - deposit", function () { @@ -359,6 +395,18 @@ describe("IntuitionFeeProxy", function () { expect(await proxy.getMultiVaultAmountFromValue(DEPOSIT_FEE)).to.equal(0n); expect(await proxy.getMultiVaultAmountFromValue(ethers.parseEther("0.05"))).to.equal(0n); }); + + it("Should revert when deposit receiver is not the caller", async function () { + const { proxy, user, nonAdmin } = await loadFixture(deployFixture); + + const desiredDepositAmount = ethers.parseEther("1"); + const totalToSend = await proxy.getTotalDepositCost(desiredDepositAmount); + const termId = ethers.zeroPadValue("0x01", 32); + + await expect( + proxy.connect(user).deposit(nonAdmin.address, termId, 1n, 0n, { value: totalToSend }) + ).to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_ReceiverMismatch"); + }); }); describe("Proxy Functions - depositBatch", function () { @@ -396,6 +444,23 @@ describe("IntuitionFeeProxy", function () { proxy.connect(user).depositBatch(user.address, termIds, curveIds, assets, minShares, { value: ethers.parseEther("20") }) ).to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_WrongArrayLengths"); }); + + it("Should revert when depositBatch receiver is not the caller", async function () { + const { proxy, user, nonAdmin } = await loadFixture(deployFixture); + + const termIds = [ethers.zeroPadValue("0x01", 32), ethers.zeroPadValue("0x02", 32)]; + const curveIds = [1n, 1n]; + const assets = [ethers.parseEther("1"), ethers.parseEther("1")]; + const minShares = [0n, 0n]; + + const totalDeposit = ethers.parseEther("2"); + const fee = await proxy.calculateDepositFee(2n, totalDeposit); + const totalRequired = totalDeposit + fee; + + await expect( + proxy.connect(user).depositBatch(nonAdmin.address, termIds, curveIds, assets, minShares, { value: totalRequired }) + ).to.be.revertedWithCustomError(proxy, "IntuitionFeeProxy_ReceiverMismatch"); + }); }); describe("View Functions (Passthrough)", function () {