diff --git a/contracts/extensions/AmountGetterWithFee.sol b/contracts/extensions/AmountGetterWithFee.sol index 9c726ca4..584fbf18 100644 --- a/contracts/extensions/AmountGetterWithFee.sol +++ b/contracts/extensions/AmountGetterWithFee.sol @@ -10,8 +10,13 @@ import { AmountGetterBase } from "./AmountGetterBase.sol"; /// @title Price getter contract that adds fee calculation contract AmountGetterWithFee is AmountGetterBase { /// @dev Allows fees in range [1e-5, 0.65535] - uint256 internal constant _FEE_BASE = 1e5; - uint256 internal constant _DISCOUNT_BASE = 100; + uint256 internal constant _BASE_1E5 = 1e5; + uint256 internal constant _BASE_1E2 = 100; + + error InvalidIntegratorFee(); + error InvalidIntegratorShare(); + error InvalidResolverFee(); + error InvalidWhitelistDiscountNumerator(); /** * @dev Calculates makingAmount with fee. @@ -26,11 +31,11 @@ contract AmountGetterWithFee is AmountGetterBase { bytes calldata extraData ) internal view virtual override returns (uint256) { unchecked { - (, uint256 integratorFee, uint256 resolverFee, bytes calldata tail) = _parseFeeData(extraData, taker, _isWhitelistedGetterImpl); + (, uint256 integratorFee, , uint256 resolverFee, bytes calldata tail) = _parseFeeData(extraData, taker, _isWhitelistedGetterImpl); return Math.mulDiv( super._getMakingAmount(order, extension, orderHash, taker, takingAmount, remainingMakingAmount, tail), - _FEE_BASE, - _FEE_BASE + integratorFee + resolverFee + _BASE_1E5, + _BASE_1E5 + integratorFee + resolverFee ); } } @@ -48,11 +53,11 @@ contract AmountGetterWithFee is AmountGetterBase { bytes calldata extraData ) internal view virtual override returns (uint256) { unchecked { - (, uint256 integratorFee, uint256 resolverFee, bytes calldata tail) = _parseFeeData(extraData, taker, _isWhitelistedGetterImpl); + (, uint256 integratorFee, , uint256 resolverFee, bytes calldata tail) = _parseFeeData(extraData, taker, _isWhitelistedGetterImpl); return Math.mulDiv( super._getTakingAmount(order, extension, orderHash, taker, makingAmount, remainingMakingAmount, tail), - _FEE_BASE + integratorFee + resolverFee, - _FEE_BASE, + _BASE_1E5 + integratorFee + resolverFee, + _BASE_1E5, Math.Rounding.Ceil ); } @@ -61,6 +66,7 @@ contract AmountGetterWithFee is AmountGetterBase { /** * @dev `extraData` consists of: * 2 bytes — integrator fee percentage (in 1e5) + * 1 byte - integrator share percentage (in 1e2) * 2 bytes — resolver fee percentage (in 1e5) * 1 byte - whitelist discount numerator (in 1e2) * bytes — whitelist structure determined by `_isWhitelisted` implementation @@ -71,14 +77,19 @@ contract AmountGetterWithFee is AmountGetterBase { bytes calldata extraData, address taker, function (bytes calldata, address) internal view returns (bool, bytes calldata) _isWhitelisted - ) internal view returns (bool isWhitelisted, uint256 integratorFee, uint256 resolverFee, bytes calldata tail) { + ) internal view returns (bool isWhitelisted, uint256 integratorFee, uint256 integratorShare, uint256 resolverFee, bytes calldata tail) { unchecked { integratorFee = uint256(uint16(bytes2(extraData))); - resolverFee = uint256(uint16(bytes2(extraData[2:]))); - uint256 whitelistDiscountNumerator = uint256(uint8(bytes1(extraData[4:]))); - (isWhitelisted, tail) = _isWhitelisted(extraData[5:], taker); + if (integratorFee > _BASE_1E5) revert InvalidIntegratorFee(); + integratorShare = uint256(uint8(bytes1(extraData[2:]))); + if (integratorShare > _BASE_1E2) revert InvalidIntegratorShare(); + resolverFee = uint256(uint16(bytes2(extraData[3:]))); + if (resolverFee > _BASE_1E5) revert InvalidResolverFee(); + uint256 whitelistDiscountNumerator = uint256(uint8(bytes1(extraData[5:]))); + if (whitelistDiscountNumerator > _BASE_1E2) revert InvalidWhitelistDiscountNumerator(); + (isWhitelisted, tail) = _isWhitelisted(extraData[6:], taker); if (isWhitelisted) { - resolverFee = resolverFee * whitelistDiscountNumerator / _DISCOUNT_BASE; + resolverFee = resolverFee * whitelistDiscountNumerator / _BASE_1E2; } } } diff --git a/contracts/extensions/FeeTaker.sol b/contracts/extensions/FeeTaker.sol index e808ce06..829dbf7d 100644 --- a/contracts/extensions/FeeTaker.sol +++ b/contracts/extensions/FeeTaker.sol @@ -102,9 +102,11 @@ contract FeeTaker is IPostInteraction, AmountGetterWithFee, Ownable { * @dev Takes the fee in taking tokens and transfers the rest to the maker. * `extraData` consists of: * 1 byte - flags - * 20 bytes — fee recipient + * 20 bytes — integrator fee recipient + * 20 bytes - protocol fee recipient * 20 bytes — receiver of taking tokens (optional, if not set, maker is used) * 2 bytes — integrator fee percentage (in 1e5) + * 1 bytes - integrator rev share percentage (in 1e2) * 2 bytes — resolver fee percentage (in 1e5) * bytes — whitelist structure determined by `_isWhitelistedPostInteractionImpl` implementation * bytes — custom data to call extra postInteraction (optional) @@ -121,34 +123,47 @@ contract FeeTaker is IPostInteraction, AmountGetterWithFee, Ownable { ) internal virtual { unchecked { bool customReceiver = extraData[0] & _CUSTOM_RECEIVER_FLAG == _CUSTOM_RECEIVER_FLAG; - address feeRecipient = address(bytes20(extraData[1:21])); - extraData = extraData[21:]; + address integratorFeeRecipient = address(bytes20(extraData[1:21])); + address protocolFeeRecipient = address(bytes20(extraData[21:41])); + extraData = extraData[41:]; address receiver = order.maker.get(); if (customReceiver) { receiver = address(bytes20(extraData)); extraData = extraData[20:]; } - (bool isWhitelisted, uint256 integratorFee, uint256 resolverFee, bytes calldata tail) = _parseFeeData(extraData, taker, _isWhitelistedPostInteractionImpl); + (bool isWhitelisted, uint256 integratorFee, uint256 integratorShare, uint256 resolverFee, bytes calldata tail) = _parseFeeData(extraData, taker, _isWhitelistedPostInteractionImpl); if (!isWhitelisted && _ACCESS_TOKEN.balanceOf(taker) == 0) revert OnlyWhitelistOrAccessToken(); - uint256 denominator = _FEE_BASE + integratorFee + resolverFee; - // fee is calculated as a sum of separate fees to limit rounding errors - uint256 fee = Math.mulDiv(takingAmount, integratorFee, denominator) + Math.mulDiv(takingAmount, resolverFee, denominator); + uint256 integratorFeeAmount; + uint256 protocolFeeAmount; + + { + uint256 denominator = _BASE_1E5 + integratorFee + resolverFee; + uint256 integratorFeeTotal = Math.mulDiv(takingAmount, integratorFee, denominator); + integratorFeeAmount = Math.mulDiv(integratorFeeTotal, integratorShare, _BASE_1E2); + protocolFeeAmount = Math.mulDiv(takingAmount, resolverFee, denominator) + integratorFeeTotal - integratorFeeAmount; + } if (order.receiver.get() == address(this)) { if (order.takerAsset.get() == address(_WETH) && order.makerTraits.unwrapWeth()) { - if (fee > 0) { - _sendEth(feeRecipient, fee); + if (integratorFeeAmount > 0) { + _sendEth(integratorFeeRecipient, integratorFeeAmount); } - _sendEth(receiver, takingAmount - fee); + if (protocolFeeAmount > 0) { + _sendEth(protocolFeeRecipient, protocolFeeAmount); + } + _sendEth(receiver, takingAmount - integratorFeeAmount - protocolFeeAmount); } else { - if (fee > 0) { - IERC20(order.takerAsset.get()).safeTransfer(feeRecipient, fee); + if (integratorFeeAmount > 0) { + IERC20(order.takerAsset.get()).safeTransfer(integratorFeeRecipient, integratorFeeAmount); + } + if (protocolFeeAmount > 0) { + IERC20(order.takerAsset.get()).safeTransfer(protocolFeeRecipient, protocolFeeAmount); } - IERC20(order.takerAsset.get()).safeTransfer(receiver, takingAmount - fee); + IERC20(order.takerAsset.get()).safeTransfer(receiver, takingAmount - integratorFeeAmount - protocolFeeAmount); } - } else if (fee > 0) { + } else if (integratorFeeAmount + protocolFeeAmount > 0) { revert InconsistentFee(); } diff --git a/package.json b/package.json index 6845351b..4304bdb7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@1inch/limit-order-protocol-contract", - "version": "4.2.1", + "version": "4.2.2", "description": "1inch Limit Order Protocol", "repository": { "type": "git", diff --git a/test/FeeTaker.js b/test/FeeTaker.js index c9d31cab..a7d29aa8 100644 --- a/test/FeeTaker.js +++ b/test/FeeTaker.js @@ -7,9 +7,9 @@ const { buildOrder, buildTakerTraits, signOrder, buildMakerTraits, buildFeeTaker const { ether } = require('./helpers/utils'); describe('FeeTaker', function () { - let addr, addr1, addr2, addr3; + let addr, addr1, addr2, addr3, addr4; before(async function () { - [addr, addr1, addr2, addr3] = await ethers.getSigners(); + [addr, addr1, addr2, addr3, addr4] = await ethers.getSigners(); }); async function deployContractsAndInit () { @@ -100,7 +100,8 @@ describe('FeeTaker', function () { const takingAmount = ether('0.3'); const integratorFee = BigInt(1e4); const resolverFee = BigInt(1e3); - const feeRecipient = addr2.address; + const integratorFeeRecipient = addr2.address; + const protocolFeeRecipient = addr3.address; const whitelist = '0x0a' + addr.address.slice(-20).repeat(10); const order = buildOrder( @@ -114,7 +115,8 @@ describe('FeeTaker', function () { }, buildFeeTakerExtensions({ feeTaker: await feeTaker.getAddress(), - feeRecipient, + integratorFeeRecipient, + protocolFeeRecipient, integratorFee, resolverFee, whitelist, @@ -129,9 +131,15 @@ describe('FeeTaker', function () { const fillTx = swap.fillOrderArgs(order, r, vs, makingAmount, takerTraits.traits, takerTraits.args); console.log(`GasUsed: ${(await (await fillTx).wait()).gasUsed.toString()}`); - const feeCalculated = takingAmount * (integratorFee + resolverFee / 2n) / BigInt(1e5); + const integratorFeeCalculated = takingAmount * (integratorFee / 2n) / BigInt(1e5); + const protocolFeeCalculated = takingAmount * (integratorFee / 2n + resolverFee / 2n) / BigInt(1e5); + const totalFeeCalculated = protocolFeeCalculated + integratorFeeCalculated; await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [makingAmount, -makingAmount]); - await expect(fillTx).to.changeTokenBalances(weth, [addr, addr1, feeRecipient], [-takingAmount - feeCalculated, takingAmount, feeCalculated]); + await expect(fillTx).to.changeTokenBalances( + weth, + [addr, addr1, integratorFeeRecipient, protocolFeeRecipient], + [-takingAmount - totalFeeCalculated, takingAmount, integratorFeeCalculated, protocolFeeCalculated], + ); }); it('should charge fee when out of whitelist', async function () { @@ -141,7 +149,8 @@ describe('FeeTaker', function () { const takingAmount = ether('0.3'); const integratorFee = BigInt(1e4); const resolverFee = BigInt(1e3); - const feeRecipient = addr2.address; + const integratorFeeRecipient = addr2.address; + const protocolFeeRecipient = addr3.address; const whitelist = '0x0a' + addr2.address.slice(-20).repeat(10); const order = buildOrder( @@ -155,7 +164,8 @@ describe('FeeTaker', function () { }, buildFeeTakerExtensions({ feeTaker: await feeTaker.getAddress(), - feeRecipient, + integratorFeeRecipient, + protocolFeeRecipient, integratorFee, resolverFee, whitelist, @@ -170,11 +180,13 @@ describe('FeeTaker', function () { const fillTx = swap.fillOrderArgs(order, r, vs, makingAmount, takerTraits.traits, takerTraits.args); console.log(`GasUsed: ${(await (await fillTx).wait()).gasUsed.toString()}`); - const feeCalculated = takingAmount * (integratorFee + resolverFee) / BigInt(1e5); + const integratorFeeCalculated = takingAmount * (integratorFee / 2n) / BigInt(1e5); + const protocolFeeCalculated = takingAmount * (integratorFee / 2n + resolverFee) / BigInt(1e5); + const totalFeeCalculated = protocolFeeCalculated + integratorFeeCalculated; await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [makingAmount, -makingAmount]); await expect(fillTx).to.changeTokenBalances(weth, - [addr, addr1, feeRecipient], - [-takingAmount - feeCalculated, takingAmount, feeCalculated], + [addr, addr1, integratorFeeRecipient, protocolFeeRecipient], + [-takingAmount - totalFeeCalculated, takingAmount, integratorFeeCalculated, protocolFeeCalculated], ); }); @@ -184,9 +196,9 @@ describe('FeeTaker', function () { const makingAmount = ether('300'); const takingAmount = ether('0.3'); const integratorFee = BigInt(1e4); - const feeCalculated = takingAmount * integratorFee / BigInt(1e5); - const feeRecipient = addr2.address; - const makerReceiver = addr3.address; + const integratorFeeRecipient = addr2.address; + const protocolFeeRecipient = addr3.address; + const makerReceiver = addr4.address; const order = buildOrder( { @@ -199,7 +211,8 @@ describe('FeeTaker', function () { }, buildFeeTakerExtensions({ feeTaker: await feeTaker.getAddress(), - feeRecipient, + integratorFeeRecipient, + protocolFeeRecipient, makerReceiver, integratorFee, }), @@ -211,8 +224,16 @@ describe('FeeTaker', function () { }); const fillTx = swap.fillOrderArgs(order, r, vs, makingAmount, takerTraits.traits, takerTraits.args); console.log(`GasUsed: ${(await (await fillTx).wait()).gasUsed.toString()}`); + + const integratorFeeCalculated = takingAmount * integratorFee / 2n / BigInt(1e5); + const protocolFeeCalculated = takingAmount * integratorFee / 2n / BigInt(1e5); + const totalFeeCalculated = integratorFeeCalculated + protocolFeeCalculated; + await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [makingAmount, -makingAmount]); - await expect(fillTx).to.changeTokenBalances(weth, [addr, addr1, feeRecipient, makerReceiver], [-takingAmount - feeCalculated, 0, feeCalculated, takingAmount]); + await expect(fillTx).to.changeTokenBalances( + weth, + [addr, addr1, integratorFeeRecipient, protocolFeeRecipient, makerReceiver], + [-takingAmount - totalFeeCalculated, 0, integratorFeeCalculated, protocolFeeCalculated, takingAmount]); }); it('should charge fee in eth', async function () { @@ -222,7 +243,8 @@ describe('FeeTaker', function () { const takingAmount = ether('0.3'); const integratorFee = BigInt(1e4); const feeCalculated = takingAmount * integratorFee / BigInt(1e5); - const feeRecipient = addr2.address; + const integratorFeeRecipient = addr2.address; + const protocolFeeRecipient = addr3.address; const order = buildOrder( { @@ -236,7 +258,8 @@ describe('FeeTaker', function () { }, buildFeeTakerExtensions({ feeTaker: await feeTaker.getAddress(), - feeRecipient, + integratorFeeRecipient, + protocolFeeRecipient, integratorFee, }), ); @@ -249,7 +272,7 @@ describe('FeeTaker', function () { console.log(`GasUsed: ${(await (await fillTx).wait()).gasUsed.toString()}`); await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [makingAmount, -makingAmount]); await expect(fillTx).to.changeTokenBalance(weth, addr, -takingAmount - feeCalculated); - await expect(fillTx).to.changeEtherBalances([addr1, feeRecipient], [takingAmount, feeCalculated]); + await expect(fillTx).to.changeEtherBalances([addr1, integratorFeeRecipient, protocolFeeRecipient], [takingAmount, feeCalculated / 2n, feeCalculated / 2n]); }); it('should charge fee in eth and send the rest to the maker receiver', async function () { @@ -259,8 +282,9 @@ describe('FeeTaker', function () { const takingAmount = ether('0.3'); const integratorFee = BigInt(1e4); const feeCalculated = takingAmount * integratorFee / BigInt(1e5); - const feeRecipient = addr2.address; - const makerReceiver = addr3.address; + const integratorFeeRecipient = addr2.address; + const protocolFeeRecipient = addr3.address; + const makerReceiver = addr4.address; const order = buildOrder( { @@ -274,7 +298,8 @@ describe('FeeTaker', function () { }, buildFeeTakerExtensions({ feeTaker: await feeTaker.getAddress(), - feeRecipient, + integratorFeeRecipient, + protocolFeeRecipient, makerReceiver, integratorFee, }), @@ -288,6 +313,6 @@ describe('FeeTaker', function () { console.log(`GasUsed: ${(await (await fillTx).wait()).gasUsed.toString()}`); await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [makingAmount, -makingAmount]); await expect(fillTx).to.changeTokenBalance(weth, addr, -takingAmount - feeCalculated); - await expect(fillTx).to.changeEtherBalances([addr1, feeRecipient, makerReceiver], [0, feeCalculated, takingAmount]); + await expect(fillTx).to.changeEtherBalances([addr1, integratorFeeRecipient, protocolFeeRecipient, makerReceiver], [0, feeCalculated / 2n, feeCalculated / 2n, takingAmount]); }); }); diff --git a/test/helpers/orderUtils.js b/test/helpers/orderUtils.js index 74ab54c3..358941c6 100644 --- a/test/helpers/orderUtils.js +++ b/test/helpers/orderUtils.js @@ -127,9 +127,11 @@ function buildMakerTraits ({ function buildFeeTakerExtensions ({ feeTaker, getterExtraPrefix = '0x', - feeRecipient = constants.ZERO_ADDRESS, + integratorFeeRecipient = constants.ZERO_ADDRESS, + protocolFeeRecipient = constants.ZERO_ADDRESS, makerReceiver = undefined, integratorFee = 0, + integratorShare = 50, resolverFee = 0, whitelistDiscount = 50, whitelist = '0x00', @@ -140,21 +142,21 @@ function buildFeeTakerExtensions ({ }) { return { makingAmountData: ethers.solidityPacked( - ['address', 'bytes', 'uint16', 'uint16', 'uint8', 'bytes', 'bytes'], - [feeTaker, getterExtraPrefix, integratorFee, resolverFee, whitelistDiscount, whitelist, customMakingGetter], + ['address', 'bytes', 'uint16', 'uint8', 'uint16', 'uint8', 'bytes', 'bytes'], + [feeTaker, getterExtraPrefix, integratorFee, integratorShare, resolverFee, whitelistDiscount, whitelist, customMakingGetter], ), takingAmountData: ethers.solidityPacked( - ['address', 'bytes', 'uint16', 'uint16', 'uint8', 'bytes', 'bytes'], - [feeTaker, getterExtraPrefix, integratorFee, resolverFee, whitelistDiscount, whitelist, customTakingGetter], + ['address', 'bytes', 'uint16', 'uint8', 'uint16', 'uint8', 'bytes', 'bytes'], + [feeTaker, getterExtraPrefix, integratorFee, integratorShare, resolverFee, whitelistDiscount, whitelist, customTakingGetter], ), postInteraction: ethers.solidityPacked( - ['address', 'bytes1', 'address'].concat( + ['address', 'bytes1', 'address', 'address'].concat( makerReceiver ? ['address'] : [], - ['uint16', 'uint16', 'uint8', 'bytes', 'bytes'], + ['uint16', 'uint8', 'uint16', 'uint8', 'bytes', 'bytes'], ), - [feeTaker, makerReceiver ? '0x01' : '0x00', feeRecipient].concat( + [feeTaker, makerReceiver ? '0x01' : '0x00', integratorFeeRecipient, protocolFeeRecipient].concat( makerReceiver ? [makerReceiver] : [], - [integratorFee, resolverFee, whitelistDiscount, whitelistPostInteraction, customPostInteraction], + [integratorFee, integratorShare, resolverFee, whitelistDiscount, whitelistPostInteraction, customPostInteraction], ), ), };