diff --git a/contracts/extensions/FeeTaker.sol b/contracts/extensions/FeeTaker.sol index 345d6b4a..69c240c0 100644 --- a/contracts/extensions/FeeTaker.sol +++ b/contracts/extensions/FeeTaker.sol @@ -7,13 +7,15 @@ import { SafeERC20 } from "@1inch/solidity-utils/contracts/libraries/SafeERC20.s import { UniERC20 } from "@1inch/solidity-utils/contracts/libraries/UniERC20.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { IAmountGetter } from "../interfaces/IAmountGetter.sol"; import { IOrderMixin } from "../interfaces/IOrderMixin.sol"; import { IPostInteraction } from "../interfaces/IPostInteraction.sol"; import { MakerTraits, MakerTraitsLib } from "../libraries/MakerTraitsLib.sol"; /// @title Helper contract that adds feature of collecting fee in takerAsset -contract FeeTaker is IPostInteraction, Ownable { +contract FeeTaker is IPostInteraction, IAmountGetter, Ownable { using AddressLib for Address; using SafeERC20 for IERC20; using UniERC20 for IERC20; @@ -55,11 +57,76 @@ contract FeeTaker is IPostInteraction, Ownable { */ receive() external payable {} + /** + * @dev Calculate makingAmount with fee. + * `extraData` consists of: + * 2 bytes — integrator fee percentage (in 1e5) + * 2 bytes — resolver fee percentage (in 1e5) + * 1 byte - taker whitelist size + * (bytes10)[N] — taker whitelist + */ + function getMakingAmount( + IOrderMixin.Order calldata order, + bytes calldata extension, + bytes32 orderHash, + address taker, + uint256 takingAmount, + uint256 remainingMakingAmount, + bytes calldata extraData + ) external view returns (uint256 calculatedMakingAmount) { + unchecked { + (uint256 integratorFee, uint256 resolverFee, bytes calldata tail) = _parseFeeData(extraData, taker); + if (tail.length > 20) { + calculatedMakingAmount = IAmountGetter(address(bytes20(tail))).getMakingAmount( + order, extension, orderHash, taker, takingAmount, remainingMakingAmount, tail[20:] + ); + } else { + calculatedMakingAmount = order.makingAmount; + } + calculatedMakingAmount = Math.mulDiv(calculatedMakingAmount, _FEE_BASE, _FEE_BASE + integratorFee + resolverFee, Math.Rounding.Floor); + return Math.mulDiv(calculatedMakingAmount, takingAmount, order.takingAmount, Math.Rounding.Floor); + } + } + + /** + * @dev Calculate takingAmount with fee. + * `extraData` consists of: + * 2 bytes — integrator fee percentage (in 1e5) + * 2 bytes — resolver fee percentage (in 1e5) + * 1 byte - taker whitelist size + * (bytes10)[N] — taker whitelist + */ + function getTakingAmount( + IOrderMixin.Order calldata order, + bytes calldata extension, + bytes32 orderHash, + address taker, + uint256 makingAmount, + uint256 remainingMakingAmount, + bytes calldata extraData + ) external view returns (uint256 calculatedTakingAmount) { + unchecked { + (uint256 integratorFee, uint256 resolverFee, bytes calldata tail) = _parseFeeData(extraData, taker); + if (tail.length > 20) { + calculatedTakingAmount = IAmountGetter(address(bytes20(tail))).getTakingAmount( + order, extension, orderHash, taker, makingAmount, remainingMakingAmount, tail[20:] + ); + } else { + calculatedTakingAmount = order.takingAmount; + } + calculatedTakingAmount = Math.mulDiv(calculatedTakingAmount, _FEE_BASE + integratorFee + resolverFee, _FEE_BASE, Math.Rounding.Ceil); + return Math.mulDiv(calculatedTakingAmount, makingAmount, order.makingAmount, Math.Rounding.Ceil); + } + } + /** * @notice See {IPostInteraction-postInteraction}. * @dev Takes the fee in taking tokens and transfers the rest to the maker. * `extraData` consists of: - * 2 bytes — fee percentage (in 1e5) + * 2 bytes — integrator fee percentage (in 1e5) + * 2 bytes — resolver fee percentage (in 1e5) + * 1 byte - taker whitelist size + * (bytes10)[N] — taker whitelist * 20 bytes — fee recipient * 20 bytes — receiver of taking tokens (optional, if not set, maker is used) */ @@ -67,34 +134,34 @@ contract FeeTaker is IPostInteraction, Ownable { IOrderMixin.Order calldata order, bytes calldata /* extension */, bytes32 /* orderHash */, - address /* taker */, + address taker, uint256 /* makingAmount */, uint256 takingAmount, uint256 /* remainingMakingAmount */, bytes calldata extraData ) external onlyLimitOrderProtocol { - uint256 fee = takingAmount * uint256(uint16(bytes2(extraData))) / _FEE_BASE; - address feeRecipient = address(bytes20(extraData[2:22])); - - address receiver = order.maker.get(); - if (extraData.length > 22) { - receiver = address(bytes20(extraData[22:42])); - } - - bool isEth = order.takerAsset.get() == address(_WETH) && order.makerTraits.unwrapWeth(); - - if (isEth) { - if (fee > 0) { - _sendEth(feeRecipient, fee); + unchecked { + (uint256 integratorFee, uint256 resolverFee, bytes calldata tail) = _parseFeeData(extraData, taker); + address feeRecipient = address(bytes20(tail)); + tail = tail[20:]; + 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); + + address receiver = order.maker.get(); + if (tail.length > 0) { + receiver = address(bytes20(tail)); } - unchecked { + + if (order.takerAsset.get() == address(_WETH) && order.makerTraits.unwrapWeth()) { + if (fee > 0) { + _sendEth(feeRecipient, fee); + } _sendEth(receiver, takingAmount - fee); - } - } else { - if (fee > 0) { - IERC20(order.takerAsset.get()).safeTransfer(feeRecipient, fee); - } - unchecked { + } else { + if (fee > 0) { + IERC20(order.takerAsset.get()).safeTransfer(feeRecipient, fee); + } IERC20(order.takerAsset.get()).safeTransfer(receiver, takingAmount - fee); } } @@ -115,4 +182,42 @@ contract FeeTaker is IPostInteraction, Ownable { revert EthTransferFailed(); } } + + /** + * @dev Validates whether the resolver is whitelisted. + * @param whitelist Whitelist is tightly packed struct of the following format: + * ``` + * (bytes10)[N] resolversAddresses; + * ``` + * Only 10 lowest bytes of the resolver address are used for comparison. + * @param resolver The resolver to check. + * @return Whether the resolver is whitelisted. + */ + function _isWhitelisted(bytes calldata whitelist, address resolver) private pure returns (bool) { + unchecked { + uint80 maskedResolverAddress = uint80(uint160(resolver)); + uint256 size = whitelist.length / 10; + for (uint256 i = 0; i < size; i++) { + uint80 whitelistedAddress = uint80(bytes10(whitelist[:10])); + if (maskedResolverAddress == whitelistedAddress) { + return true; + } + whitelist = whitelist[10:]; + } + return false; + } + } + + function _parseFeeData(bytes calldata extraData, address taker) private pure returns (uint256 integratorFee, uint256 resolverFee, bytes calldata tail) { + unchecked { + integratorFee = uint256(uint16(bytes2(extraData))); + resolverFee = uint256(uint16(bytes2(extraData[2:]))); + uint256 whitelistEnd = 5 + 10 * uint256(uint8(extraData[4])); + bytes calldata whitelist = extraData[5:whitelistEnd]; + if (!_isWhitelisted(whitelist, taker)) { + resolverFee *= 2; + } + tail = extraData[whitelistEnd:]; + } + } } diff --git a/test/FeeTaker.js b/test/FeeTaker.js index fa0d3735..cdf5c942 100644 --- a/test/FeeTaker.js +++ b/test/FeeTaker.js @@ -4,7 +4,7 @@ const { expect } = require('@1inch/solidity-utils'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { deploySwapTokens } = require('./helpers/fixtures'); const { buildOrder, buildTakerTraits, signOrder, buildMakerTraits } = require('./helpers/orderUtils'); -const { ether, trim0x } = require('./helpers/utils'); +const { ether } = require('./helpers/utils'); describe('FeeTaker', function () { let addr, addr1, addr2, addr3; @@ -53,7 +53,10 @@ describe('FeeTaker', function () { takingAmount, }, { - postInteraction: await feeTaker.getAddress() + trim0x(ethers.solidityPacked(['uint16', 'address'], [fee, feeRecipient])), + postInteraction: ethers.solidityPacked( + ['address', 'uint16', 'uint16', 'bytes1', 'address'], + [await feeTaker.getAddress(), fee, fee, '0x00', feeRecipient], + ), }, ); @@ -62,6 +65,7 @@ describe('FeeTaker', function () { extension: order.extension, }); const fillTx = swap.fillOrderArgs(order, r, vs, makingAmount, takerTraits.traits, takerTraits.args); + console.log(`GasUsed: ${(await (await fillTx).wait()).gasUsed.toString()}`); await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [makingAmount, -makingAmount]); await expect(fillTx).to.changeTokenBalances(weth, [addr, addr1, addr2], [-takingAmount, takingAmount, 0]); }); @@ -85,8 +89,10 @@ describe('FeeTaker', function () { takingAmount, }, { - postInteraction: await feeTaker.getAddress() + - trim0x(ethers.solidityPacked(['uint16', 'address', 'address'], [fee, feeRecipient, makerReceiver])), + postInteraction: ethers.solidityPacked( + ['address', 'uint16', 'uint16', 'bytes1', 'address', 'address'], + [await feeTaker.getAddress(), fee, fee, '0x00', feeRecipient, makerReceiver], + ), }, ); @@ -95,18 +101,73 @@ describe('FeeTaker', function () { extension: order.extension, }); const fillTx = swap.fillOrderArgs(order, r, vs, makingAmount, takerTraits.traits, takerTraits.args); + console.log(`GasUsed: ${(await (await fillTx).wait()).gasUsed.toString()}`); await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [makingAmount, -makingAmount]); await expect(fillTx).to.changeTokenBalances(weth, [addr, addr1, addr2, addr3], [-takingAmount, 0, 0, takingAmount]); }); - it('should charge fee', async function () { + it('should charge fee when in whitelist', async function () { const { dai, weth, swap, chainId, feeTaker } = await loadFixture(deployContractsAndInit); const makingAmount = ether('300'); const takingAmount = ether('0.3'); - const fee = BigInt(1e4); - const feeCalculated = takingAmount * fee / BigInt(1e5); + const integratorFee = BigInt(1e4); + const resolverFee = BigInt(1e3); + const feeRecipient = addr2.address; + const whitelist = '0x' + addr.address.slice(-20).repeat(10); + + const order = buildOrder( + { + maker: addr1.address, + receiver: await feeTaker.getAddress(), + makerAsset: await dai.getAddress(), + takerAsset: await weth.getAddress(), + makingAmount, + takingAmount, + }, + { + // * 2 bytes — integrator fee percentage (in 1e5) + // * 2 bytes — resolver fee percentage (in 1e5) + // * 20 bytes — fee recipient + // * 1 byte - taker whitelist size + // * (bytes10)[N] — taker whitelist + postInteraction: ethers.solidityPacked( + ['address', 'uint16', 'uint16', 'bytes1', 'bytes', 'address'], + [await feeTaker.getAddress(), integratorFee, resolverFee, '0x0a', whitelist, feeRecipient], + ), + makingAmountData: ethers.solidityPacked( + ['address', 'uint16', 'uint16', 'bytes1', 'bytes'], + [await feeTaker.getAddress(), integratorFee, resolverFee, '0x0a', whitelist], + ), + takingAmountData: ethers.solidityPacked( + ['address', 'uint16', 'uint16', 'bytes1', 'bytes'], + [await feeTaker.getAddress(), integratorFee, resolverFee, '0x0a', whitelist], + ), + }, + ); + + const { r, yParityAndS: vs } = ethers.Signature.from(await signOrder(order, chainId, await swap.getAddress(), addr1)); + const takerTraits = buildTakerTraits({ + makingAmount: true, + extension: order.extension, + }); + 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); + await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [makingAmount, -makingAmount]); + await expect(fillTx).to.changeTokenBalances(weth, [addr, addr1, addr2], [-takingAmount - feeCalculated, takingAmount, feeCalculated]); + }); + + it('should charge fee when out of whitelist', async function () { + const { dai, weth, swap, chainId, feeTaker } = await loadFixture(deployContractsAndInit); + + const makingAmount = ether('300'); + const takingAmount = ether('0.3'); + const integratorFee = BigInt(1e4); + const resolverFee = BigInt(1e3); const feeRecipient = addr2.address; + const whitelist = '0x' + addr2.address.slice(-20).repeat(10); const order = buildOrder( { @@ -118,17 +179,40 @@ describe('FeeTaker', function () { takingAmount, }, { - postInteraction: await feeTaker.getAddress() + trim0x(ethers.solidityPacked(['uint16', 'address'], [fee, feeRecipient])), + // * 2 bytes — integrator fee percentage (in 1e5) + // * 2 bytes — resolver fee percentage (in 1e5) + // * 20 bytes — fee recipient + // * 1 byte - taker whitelist size + // * (bytes10)[N] — taker whitelist + postInteraction: ethers.solidityPacked( + ['address', 'uint16', 'uint16', 'bytes1', 'bytes', 'address'], + [await feeTaker.getAddress(), integratorFee, resolverFee, '0x0a', whitelist, feeRecipient], + ), + makingAmountData: ethers.solidityPacked( + ['address', 'uint16', 'uint16', 'bytes1', 'bytes'], + [await feeTaker.getAddress(), integratorFee, resolverFee, '0x0a', whitelist], + ), + takingAmountData: ethers.solidityPacked( + ['address', 'uint16', 'uint16', 'bytes1', 'bytes'], + [await feeTaker.getAddress(), integratorFee, resolverFee, '0x0a', whitelist], + ), }, ); const { r, yParityAndS: vs } = ethers.Signature.from(await signOrder(order, chainId, await swap.getAddress(), addr1)); const takerTraits = buildTakerTraits({ + makingAmount: true, extension: order.extension, }); 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 + resolverFee) / BigInt(1e5); await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [makingAmount, -makingAmount]); - await expect(fillTx).to.changeTokenBalances(weth, [addr, addr1, addr2], [-takingAmount, takingAmount - feeCalculated, feeCalculated]); + await expect(fillTx).to.changeTokenBalances(weth, + [addr, addr1, addr2], + [-takingAmount - feeCalculated, takingAmount, feeCalculated], + ); }); it('should charge fee and send the rest to the maker receiver', async function () { @@ -151,8 +235,18 @@ describe('FeeTaker', function () { takingAmount, }, { - postInteraction: await feeTaker.getAddress() + - trim0x(ethers.solidityPacked(['uint16', 'address', 'address'], [fee, feeRecipient, makerReceiver])), + postInteraction: ethers.solidityPacked( + ['address', 'uint16', 'uint16', 'bytes1', 'address', 'address'], + [await feeTaker.getAddress(), fee, 0, '0x00', feeRecipient, makerReceiver], + ), + makingAmountData: ethers.solidityPacked( + ['address', 'uint16', 'uint16', 'bytes1'], + [await feeTaker.getAddress(), fee, 0, '0x00'], + ), + takingAmountData: ethers.solidityPacked( + ['address', 'uint16', 'uint16', 'bytes1'], + [await feeTaker.getAddress(), fee, 0, '0x00'], + ), }, ); @@ -161,8 +255,9 @@ describe('FeeTaker', function () { extension: order.extension, }); const fillTx = swap.fillOrderArgs(order, r, vs, makingAmount, takerTraits.traits, takerTraits.args); + console.log(`GasUsed: ${(await (await fillTx).wait()).gasUsed.toString()}`); await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [makingAmount, -makingAmount]); - await expect(fillTx).to.changeTokenBalances(weth, [addr, addr1, addr2, addr3], [-takingAmount, 0, feeCalculated, takingAmount - feeCalculated]); + await expect(fillTx).to.changeTokenBalances(weth, [addr, addr1, addr2, addr3], [-takingAmount - feeCalculated, 0, feeCalculated, takingAmount]); }); it('should charge fee in eth', async function () { @@ -185,7 +280,18 @@ describe('FeeTaker', function () { makerTraits: buildMakerTraits({ unwrapWeth: true }), }, { - postInteraction: await feeTaker.getAddress() + trim0x(ethers.solidityPacked(['uint16', 'address'], [fee, feeRecipient])), + postInteraction: ethers.solidityPacked( + ['address', 'uint16', 'uint16', 'bytes1', 'address'], + [await feeTaker.getAddress(), fee, 0, '0x00', feeRecipient], + ), + makingAmountData: ethers.solidityPacked( + ['address', 'uint16', 'uint16', 'bytes1'], + [await feeTaker.getAddress(), fee, 0, '0x00'], + ), + takingAmountData: ethers.solidityPacked( + ['address', 'uint16', 'uint16', 'bytes1'], + [await feeTaker.getAddress(), fee, 0, '0x00'], + ), }, ); @@ -194,9 +300,10 @@ describe('FeeTaker', function () { extension: order.extension, }); const fillTx = swap.fillOrderArgs(order, r, vs, makingAmount, takerTraits.traits, takerTraits.args); + 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); - await expect(fillTx).to.changeEtherBalances([addr1, addr2], [takingAmount - feeCalculated, feeCalculated]); + await expect(fillTx).to.changeTokenBalance(weth, addr, -takingAmount - feeCalculated); + await expect(fillTx).to.changeEtherBalances([addr1, addr2], [takingAmount, feeCalculated]); }); it('should charge fee in eth and send the rest to the maker receiver', async function () { @@ -220,8 +327,18 @@ describe('FeeTaker', function () { makerTraits: buildMakerTraits({ unwrapWeth: true }), }, { - postInteraction: await feeTaker.getAddress() + - trim0x(ethers.solidityPacked(['uint16', 'address', 'address'], [fee, feeRecipient, makerReceiver])), + postInteraction: ethers.solidityPacked( + ['address', 'uint16', 'uint16', 'bytes1', 'address', 'address'], + [await feeTaker.getAddress(), fee, 0, '0x00', feeRecipient, makerReceiver], + ), + makingAmountData: ethers.solidityPacked( + ['address', 'uint16', 'uint16', 'bytes1'], + [await feeTaker.getAddress(), fee, 0, '0x00'], + ), + takingAmountData: ethers.solidityPacked( + ['address', 'uint16', 'uint16', 'bytes1'], + [await feeTaker.getAddress(), fee, 0, '0x00'], + ), }, ); @@ -230,8 +347,9 @@ describe('FeeTaker', function () { extension: order.extension, }); const fillTx = swap.fillOrderArgs(order, r, vs, makingAmount, takerTraits.traits, takerTraits.args); + 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); - await expect(fillTx).to.changeEtherBalances([addr1, addr2, addr3], [0, feeCalculated, takingAmount - feeCalculated]); + await expect(fillTx).to.changeTokenBalance(weth, addr, -takingAmount - feeCalculated); + await expect(fillTx).to.changeEtherBalances([addr1, addr2, addr3], [0, feeCalculated, takingAmount]); }); });