From 3e0129f0d265eae2283d58bf0fcc78ea0f21a085 Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Tue, 19 Nov 2024 17:42:00 +0400 Subject: [PATCH 01/17] upgrade FeeTaker --- contracts/extensions/AmountGetterBase.sol | 71 ++++++++ contracts/extensions/AmountGetterWithFee.sol | 113 ++++++++++++ contracts/extensions/FeeTaker.sol | 172 ++++++------------- test/FeeTaker.js | 74 ++++---- 4 files changed, 277 insertions(+), 153 deletions(-) create mode 100644 contracts/extensions/AmountGetterBase.sol create mode 100644 contracts/extensions/AmountGetterWithFee.sol diff --git a/contracts/extensions/AmountGetterBase.sol b/contracts/extensions/AmountGetterBase.sol new file mode 100644 index 00000000..8206ee09 --- /dev/null +++ b/contracts/extensions/AmountGetterBase.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.23; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; + +import { IAmountGetter } from "../interfaces/IAmountGetter.sol"; +import { IOrderMixin } from "../interfaces/IOrderMixin.sol"; + +/// @title Base price getter contract that either calls external getter or applies linear formula +contract AmountGetterBase is IAmountGetter { + function getMakingAmount( + IOrderMixin.Order calldata order, + bytes calldata extension, + bytes32 orderHash, + address taker, + uint256 takingAmount, + uint256 remainingMakingAmount, + bytes calldata extraData + ) external view returns (uint256) { + return _getMakingAmount(order, extension, orderHash, taker, takingAmount, remainingMakingAmount, extraData); + } + + function getTakingAmount( + IOrderMixin.Order calldata order, + bytes calldata extension, + bytes32 orderHash, + address taker, + uint256 makingAmount, + uint256 remainingMakingAmount, + bytes calldata extraData + ) external view returns (uint256) { + return _getTakingAmount(order, extension, orderHash, taker, makingAmount, remainingMakingAmount, extraData); + } + + function _getMakingAmount( + IOrderMixin.Order calldata order, + bytes calldata extension, + bytes32 orderHash, + address taker, + uint256 takingAmount, + uint256 remainingMakingAmount, + bytes calldata extraData + ) internal view virtual returns (uint256) { + if (extraData.length >= 20) { + return IAmountGetter(address(bytes20(extraData))).getMakingAmount( + order, extension, orderHash, taker, takingAmount, remainingMakingAmount, extraData[20:] + ); + } else { + return Math.mulDiv(order.makingAmount, takingAmount, order.takingAmount); + } + } + + function _getTakingAmount( + IOrderMixin.Order calldata order, + bytes calldata extension, + bytes32 orderHash, + address taker, + uint256 makingAmount, + uint256 remainingMakingAmount, + bytes calldata extraData + ) internal view virtual returns (uint256) { + if (extraData.length >= 20) { + return IAmountGetter(address(bytes20(extraData))).getTakingAmount( + order, extension, orderHash, taker, makingAmount, remainingMakingAmount, extraData[20:] + ); + } else { + return Math.mulDiv(order.takingAmount, makingAmount, order.makingAmount, Math.Rounding.Ceil); + } + } +} diff --git a/contracts/extensions/AmountGetterWithFee.sol b/contracts/extensions/AmountGetterWithFee.sol new file mode 100644 index 00000000..d8daa84b --- /dev/null +++ b/contracts/extensions/AmountGetterWithFee.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.23; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; + +import { IOrderMixin } from "../interfaces/IOrderMixin.sol"; +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; + + /** + * @dev Calculates makingAmount with fee. + */ + function _getMakingAmount( + IOrderMixin.Order calldata order, + bytes calldata extension, + bytes32 orderHash, + address taker, + uint256 takingAmount, + uint256 remainingMakingAmount, + bytes calldata extraData + ) internal view override returns (uint256) { + unchecked { + (, 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 + ); + } + } + + /** + * @dev Calculates takingAmount with fee. + */ + function _getTakingAmount( + IOrderMixin.Order calldata order, + bytes calldata extension, + bytes32 orderHash, + address taker, + uint256 makingAmount, + uint256 remainingMakingAmount, + bytes calldata extraData + ) internal view override returns (uint256) { + unchecked { + (, 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, + Math.Rounding.Ceil + ); + } + } + + /** + * @dev `extraData` consists of: + * 2 bytes — integrator fee percentage (in 1e5) + * 2 bytes — resolver fee percentage (in 1e5) + * 1 byte - whitelist discount numerator (in 1e2) + * bytes — whitelist structure determined by `_isWhitelisted` implementation + * bytes — custom data (optional) + * @param _isWhitelisted internal function to parse and check whitelist + */ + function _parseFeeData( + 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) { + 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 (isWhitelisted) { + resolverFee = resolverFee * whitelistDiscountNumerator / _DISCOUNT_BASE; + } + } + } + + /** + * @dev Validates whether the taker is whitelisted. + * @param whitelistData Whitelist data is a tightly packed struct of the following format: + * ``` + * 1 byte - size of the whitelist + * (bytes10)[N] whiteliested addresses; + * ``` + * Only 10 lowest bytes of the address are used for comparison. + * @param taker The taker address to check. + * @return isWhitelisted Whether the taker is whitelisted. + * @return tail Remaining calldata. + */ + function _isWhitelistedGetterImpl(bytes calldata whitelistData, address taker) internal pure returns (bool isWhitelisted, bytes calldata tail) { + unchecked { + uint80 maskedTakerAddress = uint80(uint160(taker)); + uint256 size = uint8(whitelistData[0]); + bytes calldata whitelist = whitelistData[1:1 + 10 * size]; + tail = whitelistData[1 + 10 * size:]; + for (uint256 i = 0; i < size; i++) { + uint80 whitelistedAddress = uint80(bytes10(whitelist[:10])); + if (maskedTakerAddress == whitelistedAddress) { + return (true, tail); + } + whitelist = whitelist[10:]; + } + } + } +} diff --git a/contracts/extensions/FeeTaker.sol b/contracts/extensions/FeeTaker.sol index 69c240c0..18445a13 100644 --- a/contracts/extensions/FeeTaker.sol +++ b/contracts/extensions/FeeTaker.sol @@ -9,33 +9,39 @@ 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"; +import { AmountGetterWithFee } from "./AmountGetterWithFee.sol"; /// @title Helper contract that adds feature of collecting fee in takerAsset -contract FeeTaker is IPostInteraction, IAmountGetter, Ownable { +contract FeeTaker is IPostInteraction, AmountGetterWithFee, Ownable { using AddressLib for Address; using SafeERC20 for IERC20; using UniERC20 for IERC20; using MakerTraitsLib for MakerTraits; + bytes1 private constant _CUSTOM_RECEIVER_FLAG = 0x01; + /** * @dev The caller is not the limit order protocol contract. */ error OnlyLimitOrderProtocol(); + /** + * @dev The taker is not whitelisted and does not have access token. + */ + error OnlyWhitelistOrAccessToken(); + /** * @dev Eth transfer failed. The target fallback may have reverted. */ error EthTransferFailed(); - /// @dev Allows fees in range [1e-5, 0.65535] - uint256 internal constant _FEE_BASE = 1e5; - address private immutable _LIMIT_ORDER_PROTOCOL; address private immutable _WETH; + /// @notice Contract address whose tokens allow filling limit orders with a fee for resolvers that are outside the whitelist + IERC20 private immutable _ACCESS_TOKEN; /// @dev Modifier to check if the caller is the limit order protocol contract. modifier onlyLimitOrderProtocol { @@ -46,10 +52,14 @@ contract FeeTaker is IPostInteraction, IAmountGetter, Ownable { /** * @notice Initializes the contract. * @param limitOrderProtocol The limit order protocol contract. + * @param accessToken Contract address whose tokens allow filling limit orders with a fee for resolvers that are outside the whitelist. + * @param weth The WETH address. + * @param owner The owner of the contract. */ - constructor(address limitOrderProtocol, address weth, address owner) Ownable(owner) { + constructor(address limitOrderProtocol, IERC20 accessToken, address weth, address owner) Ownable(owner) { _LIMIT_ORDER_PROTOCOL = limitOrderProtocol; _WETH = weth; + _ACCESS_TOKEN = accessToken; } /** @@ -57,101 +67,66 @@ contract FeeTaker is IPostInteraction, IAmountGetter, 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( + function postInteraction( IOrderMixin.Order calldata order, bytes calldata extension, bytes32 orderHash, address taker, + uint256 makingAmount, 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); - } + ) external onlyLimitOrderProtocol { + _postInteraction(order, extension, orderHash, taker, makingAmount, takingAmount, remainingMakingAmount, extraData); } /** - * @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 + * @notice Retrieves funds accidently sent directly to the contract address + * @param token ERC20 token to retrieve + * @param amount amount to retrieve */ - 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); - } + function rescueFunds(IERC20 token, uint256 amount) external onlyOwner { + token.uniTransfer(payable(msg.sender), amount); } /** * @notice See {IPostInteraction-postInteraction}. * @dev Takes the fee in taking tokens and transfers the rest to the maker. * `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 + * 1 byte - flags * 20 bytes — fee recipient * 20 bytes — receiver of taking tokens (optional, if not set, maker is used) + * 2 bytes — integrator fee percentage (in 1e5) + * 2 bytes — resolver fee percentage (in 1e5) + * bytes — whitelist structure determined by `_isWhitelistedPostInteractionImpl` implementation + * bytes — custom data to call extra postInteraction (optional) */ - function postInteraction( + function _postInteraction( IOrderMixin.Order calldata order, - bytes calldata /* extension */, - bytes32 /* orderHash */, + bytes calldata extension, + bytes32 orderHash, address taker, - uint256 /* makingAmount */, + uint256 makingAmount, uint256 takingAmount, - uint256 /* remainingMakingAmount */, + uint256 remainingMakingAmount, bytes calldata extraData - ) external onlyLimitOrderProtocol { + ) internal virtual { 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); + bool customReceiver = extraData[0] & _CUSTOM_RECEIVER_FLAG == _CUSTOM_RECEIVER_FLAG; + address feeRecipient = address(bytes20(extraData[1:21])); + extraData = extraData[21:]; address receiver = order.maker.get(); - if (tail.length > 0) { - receiver = address(bytes20(tail)); + if (customReceiver) { + receiver = address(bytes20(extraData)); + extraData = extraData[20:]; } + (bool isWhitelisted, uint256 integratorFee, 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); if (order.takerAsset.get() == address(_WETH) && order.makerTraits.unwrapWeth()) { if (fee > 0) { @@ -164,16 +139,19 @@ contract FeeTaker is IPostInteraction, IAmountGetter, Ownable { } IERC20(order.takerAsset.get()).safeTransfer(receiver, takingAmount - fee); } + + if (tail.length >= 20) { + IPostInteraction(address(bytes20(tail))).postInteraction(order, extension, orderHash, taker, makingAmount, takingAmount, remainingMakingAmount, tail[20:]); + } } } /** - * @notice Retrieves funds accidently sent directly to the contract address - * @param token ERC20 token to retrieve - * @param amount amount to retrieve + * @dev Parses fee data from `extraData`. + * Override this function if whitelist structure in postInteraction is different from getters. */ - function rescueFunds(IERC20 token, uint256 amount) external onlyOwner { - token.uniTransfer(payable(msg.sender), amount); + function _isWhitelistedPostInteractionImpl(bytes calldata whitelistData, address taker) internal view virtual returns (bool isWhitelisted, bytes calldata tail) { + return _isWhitelistedGetterImpl(whitelistData, taker); } function _sendEth(address target, uint256 amount) private { @@ -182,42 +160,4 @@ contract FeeTaker is IPostInteraction, IAmountGetter, 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 cdf5c942..63dbb45f 100644 --- a/test/FeeTaker.js +++ b/test/FeeTaker.js @@ -30,7 +30,7 @@ describe('FeeTaker', function () { await inch.connect(addr1).approve(swap, ether('1000000')); const FeeTaker = await ethers.getContractFactory('FeeTaker'); - const feeTaker = await FeeTaker.deploy(swap, weth, addr); + const feeTaker = await FeeTaker.deploy(swap, inch, weth, addr); return { dai, weth, inch, swap, chainId, feeTaker }; }; @@ -54,8 +54,8 @@ describe('FeeTaker', function () { }, { postInteraction: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'bytes1', 'address'], - [await feeTaker.getAddress(), fee, fee, '0x00', feeRecipient], + ['address', 'bytes1', 'address', 'uint16', 'uint16', 'uint8', 'bytes1'], + [await feeTaker.getAddress(), '0x00', feeRecipient, fee, fee, 50, '0x00'], ), }, ); @@ -90,8 +90,8 @@ describe('FeeTaker', function () { }, { postInteraction: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'bytes1', 'address', 'address'], - [await feeTaker.getAddress(), fee, fee, '0x00', feeRecipient, makerReceiver], + ['address', 'bytes1', 'address', 'address', 'uint16', 'uint16', 'uint8', 'bytes1'], + [await feeTaker.getAddress(), '0x01', feeRecipient, makerReceiver, fee, fee, 50, '0x00'], ), }, ); @@ -132,16 +132,16 @@ describe('FeeTaker', function () { // * 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], + ['address', 'bytes1', 'address', 'uint16', 'uint16', 'uint8', 'bytes1', 'bytes'], + [await feeTaker.getAddress(), '0x00', feeRecipient, integratorFee, resolverFee, 50, '0x0a', whitelist], ), makingAmountData: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'bytes1', 'bytes'], - [await feeTaker.getAddress(), integratorFee, resolverFee, '0x0a', whitelist], + ['address', 'uint16', 'uint16', 'uint8', 'bytes1', 'bytes'], + [await feeTaker.getAddress(), integratorFee, resolverFee, 50, '0x0a', whitelist], ), takingAmountData: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'bytes1', 'bytes'], - [await feeTaker.getAddress(), integratorFee, resolverFee, '0x0a', whitelist], + ['address', 'uint16', 'uint16', 'uint8', 'bytes1', 'bytes'], + [await feeTaker.getAddress(), integratorFee, resolverFee, 50, '0x0a', whitelist], ), }, ); @@ -154,7 +154,7 @@ 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 feeCalculated = takingAmount * (integratorFee + resolverFee / 2n) / 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]); }); @@ -185,16 +185,16 @@ describe('FeeTaker', function () { // * 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], + ['address', 'bytes1', 'address', 'uint16', 'uint16', 'uint8', 'bytes1', 'bytes'], + [await feeTaker.getAddress(), '0x00', feeRecipient, integratorFee, resolverFee, 50, '0x0a', whitelist], ), makingAmountData: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'bytes1', 'bytes'], - [await feeTaker.getAddress(), integratorFee, resolverFee, '0x0a', whitelist], + ['address', 'uint16', 'uint16', 'uint8', 'bytes1', 'bytes'], + [await feeTaker.getAddress(), integratorFee, resolverFee, 50, '0x0a', whitelist], ), takingAmountData: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'bytes1', 'bytes'], - [await feeTaker.getAddress(), integratorFee, resolverFee, '0x0a', whitelist], + ['address', 'uint16', 'uint16', 'uint8', 'bytes1', 'bytes'], + [await feeTaker.getAddress(), integratorFee, resolverFee, 50, '0x0a', whitelist], ), }, ); @@ -207,7 +207,7 @@ 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 + resolverFee) / BigInt(1e5); + 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], @@ -236,16 +236,16 @@ describe('FeeTaker', function () { }, { postInteraction: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'bytes1', 'address', 'address'], - [await feeTaker.getAddress(), fee, 0, '0x00', feeRecipient, makerReceiver], + ['address', 'bytes1', 'address', 'address', 'uint16', 'uint16', 'uint8', 'bytes1'], + [await feeTaker.getAddress(), '0x01', feeRecipient, makerReceiver, fee, 0, 50, '0x00'], ), makingAmountData: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'bytes1'], - [await feeTaker.getAddress(), fee, 0, '0x00'], + ['address', 'uint16', 'uint16', 'uint8', 'bytes1'], + [await feeTaker.getAddress(), fee, 0, 50, '0x00'], ), takingAmountData: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'bytes1'], - [await feeTaker.getAddress(), fee, 0, '0x00'], + ['address', 'uint16', 'uint16', 'uint8', 'bytes1'], + [await feeTaker.getAddress(), fee, 0, 50, '0x00'], ), }, ); @@ -281,16 +281,16 @@ describe('FeeTaker', function () { }, { postInteraction: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'bytes1', 'address'], - [await feeTaker.getAddress(), fee, 0, '0x00', feeRecipient], + ['address', 'bytes1', 'address', 'uint16', 'uint16', 'uint8', 'bytes1'], + [await feeTaker.getAddress(), '0x00', feeRecipient, fee, 0, 50, '0x00'], ), makingAmountData: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'bytes1'], - [await feeTaker.getAddress(), fee, 0, '0x00'], + ['address', 'uint16', 'uint16', 'uint8', 'bytes1'], + [await feeTaker.getAddress(), fee, 0, 50, '0x00'], ), takingAmountData: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'bytes1'], - [await feeTaker.getAddress(), fee, 0, '0x00'], + ['address', 'uint16', 'uint16', 'uint8', 'bytes1'], + [await feeTaker.getAddress(), fee, 0, 50, '0x00'], ), }, ); @@ -328,16 +328,16 @@ describe('FeeTaker', function () { }, { postInteraction: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'bytes1', 'address', 'address'], - [await feeTaker.getAddress(), fee, 0, '0x00', feeRecipient, makerReceiver], + ['address', 'bytes1', 'address', 'address', 'uint16', 'uint16', 'uint8', 'bytes1'], + [await feeTaker.getAddress(), '0x01', feeRecipient, makerReceiver, fee, 0, 50, '0x00'], ), makingAmountData: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'bytes1'], - [await feeTaker.getAddress(), fee, 0, '0x00'], + ['address', 'uint16', 'uint16', 'uint8', 'bytes1'], + [await feeTaker.getAddress(), fee, 0, 50, '0x00'], ), takingAmountData: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'bytes1'], - [await feeTaker.getAddress(), fee, 0, '0x00'], + ['address', 'uint16', 'uint16', 'uint8', 'bytes1'], + [await feeTaker.getAddress(), fee, 0, 50, '0x00'], ), }, ); From 93e3fa26cd72e0eedb2d8523647910489b806f69 Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Tue, 19 Nov 2024 17:47:47 +0400 Subject: [PATCH 02/17] fix package --- package.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/package.json b/package.json index 727ddc0c..3ccbf404 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,7 @@ }, "license": "MIT", "files": [ - "contracts/*.sol", - "contracts/helpers", - "contracts/interfaces", - "contracts/libraries", - "contracts/mocks", + "contracts", "test/helpers" ], "dependencies": { From ebd6934e157405a6cd81f08dd96df41291dd69ef Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Tue, 19 Nov 2024 17:47:55 +0400 Subject: [PATCH 03/17] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3ccbf404..119cd79c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@1inch/limit-order-protocol-contract", - "version": "4.1.1", + "version": "4.2.0", "description": "1inch Limit Order Protocol", "repository": { "type": "git", From fec23799007523be80eabc2341a0c3159b479323 Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Tue, 19 Nov 2024 17:56:49 +0400 Subject: [PATCH 04/17] fix --- contracts/extensions/AmountGetterWithFee.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/extensions/AmountGetterWithFee.sol b/contracts/extensions/AmountGetterWithFee.sol index d8daa84b..b8f283e5 100644 --- a/contracts/extensions/AmountGetterWithFee.sol +++ b/contracts/extensions/AmountGetterWithFee.sol @@ -24,7 +24,7 @@ contract AmountGetterWithFee is AmountGetterBase { uint256 takingAmount, uint256 remainingMakingAmount, bytes calldata extraData - ) internal view override returns (uint256) { + ) internal view virtual override returns (uint256) { unchecked { (, uint256 integratorFee, uint256 resolverFee, bytes calldata tail) = _parseFeeData(extraData, taker, _isWhitelistedGetterImpl); return Math.mulDiv( @@ -46,7 +46,7 @@ contract AmountGetterWithFee is AmountGetterBase { uint256 makingAmount, uint256 remainingMakingAmount, bytes calldata extraData - ) internal view override returns (uint256) { + ) internal view virtual override returns (uint256) { unchecked { (, uint256 integratorFee, uint256 resolverFee, bytes calldata tail) = _parseFeeData(extraData, taker, _isWhitelistedGetterImpl); return Math.mulDiv( From d721b327e847d74dd86cce399f806ba13b3c829a Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Wed, 20 Nov 2024 14:05:22 +0400 Subject: [PATCH 05/17] add feeTaker builder --- test/FeeTaker.js | 152 ++++++++++++------------------------- test/helpers/orderUtils.js | 34 +++++++++ 2 files changed, 84 insertions(+), 102 deletions(-) diff --git a/test/FeeTaker.js b/test/FeeTaker.js index 63dbb45f..eb8b51a7 100644 --- a/test/FeeTaker.js +++ b/test/FeeTaker.js @@ -3,10 +3,10 @@ const { ethers } = hre; 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 { buildOrder, buildTakerTraits, signOrder, buildMakerTraits, buildFeeTakerExtensions } = require('./helpers/orderUtils'); const { ether } = require('./helpers/utils'); -describe('FeeTaker', function () { +describe.only('FeeTaker', function () { let addr, addr1, addr2, addr3; before(async function () { [addr, addr1, addr2, addr3] = await ethers.getSigners(); @@ -40,7 +40,6 @@ describe('FeeTaker', function () { const makingAmount = ether('300'); const takingAmount = ether('0.3'); - const fee = 0; const feeRecipient = addr2.address; const order = buildOrder( @@ -52,12 +51,10 @@ describe('FeeTaker', function () { makingAmount, takingAmount, }, - { - postInteraction: ethers.solidityPacked( - ['address', 'bytes1', 'address', 'uint16', 'uint16', 'uint8', 'bytes1'], - [await feeTaker.getAddress(), '0x00', feeRecipient, fee, fee, 50, '0x00'], - ), - }, + buildFeeTakerExtensions({ + feeTaker: await feeTaker.getAddress(), + feeRecipient, + }), ); const { r, yParityAndS: vs } = ethers.Signature.from(await signOrder(order, chainId, await swap.getAddress(), addr1)); @@ -75,7 +72,6 @@ describe('FeeTaker', function () { const makingAmount = ether('300'); const takingAmount = ether('0.3'); - const fee = 0; const feeRecipient = addr2.address; const makerReceiver = addr3.address; @@ -88,12 +84,11 @@ describe('FeeTaker', function () { makingAmount, takingAmount, }, - { - postInteraction: ethers.solidityPacked( - ['address', 'bytes1', 'address', 'address', 'uint16', 'uint16', 'uint8', 'bytes1'], - [await feeTaker.getAddress(), '0x01', feeRecipient, makerReceiver, fee, fee, 50, '0x00'], - ), - }, + buildFeeTakerExtensions({ + feeTaker: await feeTaker.getAddress(), + feeRecipient, + makerReceiver, + }), ); const { r, yParityAndS: vs } = ethers.Signature.from(await signOrder(order, chainId, await swap.getAddress(), addr1)); @@ -125,25 +120,14 @@ describe('FeeTaker', function () { 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', 'bytes1', 'address', 'uint16', 'uint16', 'uint8', 'bytes1', 'bytes'], - [await feeTaker.getAddress(), '0x00', feeRecipient, integratorFee, resolverFee, 50, '0x0a', whitelist], - ), - makingAmountData: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'uint8', 'bytes1', 'bytes'], - [await feeTaker.getAddress(), integratorFee, resolverFee, 50, '0x0a', whitelist], - ), - takingAmountData: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'uint8', 'bytes1', 'bytes'], - [await feeTaker.getAddress(), integratorFee, resolverFee, 50, '0x0a', whitelist], - ), - }, + buildFeeTakerExtensions({ + feeTaker: await feeTaker.getAddress(), + feeRecipient, + integratorFee, + resolverFee, + whitelistLength: 10, + whitelist, + }), ); const { r, yParityAndS: vs } = ethers.Signature.from(await signOrder(order, chainId, await swap.getAddress(), addr1)); @@ -178,25 +162,14 @@ describe('FeeTaker', function () { 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', 'bytes1', 'address', 'uint16', 'uint16', 'uint8', 'bytes1', 'bytes'], - [await feeTaker.getAddress(), '0x00', feeRecipient, integratorFee, resolverFee, 50, '0x0a', whitelist], - ), - makingAmountData: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'uint8', 'bytes1', 'bytes'], - [await feeTaker.getAddress(), integratorFee, resolverFee, 50, '0x0a', whitelist], - ), - takingAmountData: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'uint8', 'bytes1', 'bytes'], - [await feeTaker.getAddress(), integratorFee, resolverFee, 50, '0x0a', whitelist], - ), - }, + buildFeeTakerExtensions({ + feeTaker: await feeTaker.getAddress(), + feeRecipient, + integratorFee, + resolverFee, + whitelistLength: 10, + whitelist, + }), ); const { r, yParityAndS: vs } = ethers.Signature.from(await signOrder(order, chainId, await swap.getAddress(), addr1)); @@ -220,8 +193,8 @@ describe('FeeTaker', function () { const makingAmount = ether('300'); const takingAmount = ether('0.3'); - const fee = BigInt(1e4); - const feeCalculated = takingAmount * fee / BigInt(1e5); + const integratorFee = BigInt(1e4); + const feeCalculated = takingAmount * integratorFee / BigInt(1e5); const feeRecipient = addr2.address; const makerReceiver = addr3.address; @@ -234,20 +207,12 @@ describe('FeeTaker', function () { makingAmount, takingAmount, }, - { - postInteraction: ethers.solidityPacked( - ['address', 'bytes1', 'address', 'address', 'uint16', 'uint16', 'uint8', 'bytes1'], - [await feeTaker.getAddress(), '0x01', feeRecipient, makerReceiver, fee, 0, 50, '0x00'], - ), - makingAmountData: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'uint8', 'bytes1'], - [await feeTaker.getAddress(), fee, 0, 50, '0x00'], - ), - takingAmountData: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'uint8', 'bytes1'], - [await feeTaker.getAddress(), fee, 0, 50, '0x00'], - ), - }, + buildFeeTakerExtensions({ + feeTaker: await feeTaker.getAddress(), + feeRecipient, + makerReceiver, + integratorFee, + }), ); const { r, yParityAndS: vs } = ethers.Signature.from(await signOrder(order, chainId, await swap.getAddress(), addr1)); @@ -265,8 +230,8 @@ describe('FeeTaker', function () { const makingAmount = ether('300'); const takingAmount = ether('0.3'); - const fee = BigInt(1e4); - const feeCalculated = takingAmount * fee / BigInt(1e5); + const integratorFee = BigInt(1e4); + const feeCalculated = takingAmount * integratorFee / BigInt(1e5); const feeRecipient = addr2.address; const order = buildOrder( @@ -279,20 +244,11 @@ describe('FeeTaker', function () { takingAmount, makerTraits: buildMakerTraits({ unwrapWeth: true }), }, - { - postInteraction: ethers.solidityPacked( - ['address', 'bytes1', 'address', 'uint16', 'uint16', 'uint8', 'bytes1'], - [await feeTaker.getAddress(), '0x00', feeRecipient, fee, 0, 50, '0x00'], - ), - makingAmountData: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'uint8', 'bytes1'], - [await feeTaker.getAddress(), fee, 0, 50, '0x00'], - ), - takingAmountData: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'uint8', 'bytes1'], - [await feeTaker.getAddress(), fee, 0, 50, '0x00'], - ), - }, + buildFeeTakerExtensions({ + feeTaker: await feeTaker.getAddress(), + feeRecipient, + integratorFee, + }), ); const { r, yParityAndS: vs } = ethers.Signature.from(await signOrder(order, chainId, await swap.getAddress(), addr1)); @@ -311,8 +267,8 @@ describe('FeeTaker', function () { const makingAmount = ether('300'); const takingAmount = ether('0.3'); - const fee = BigInt(1e4); - const feeCalculated = takingAmount * fee / BigInt(1e5); + const integratorFee = BigInt(1e4); + const feeCalculated = takingAmount * integratorFee / BigInt(1e5); const feeRecipient = addr2.address; const makerReceiver = addr3.address; @@ -326,20 +282,12 @@ describe('FeeTaker', function () { takingAmount, makerTraits: buildMakerTraits({ unwrapWeth: true }), }, - { - postInteraction: ethers.solidityPacked( - ['address', 'bytes1', 'address', 'address', 'uint16', 'uint16', 'uint8', 'bytes1'], - [await feeTaker.getAddress(), '0x01', feeRecipient, makerReceiver, fee, 0, 50, '0x00'], - ), - makingAmountData: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'uint8', 'bytes1'], - [await feeTaker.getAddress(), fee, 0, 50, '0x00'], - ), - takingAmountData: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'uint8', 'bytes1'], - [await feeTaker.getAddress(), fee, 0, 50, '0x00'], - ), - }, + buildFeeTakerExtensions({ + feeTaker: await feeTaker.getAddress(), + feeRecipient, + makerReceiver, + integratorFee, + }), ); const { r, yParityAndS: vs } = ethers.Signature.from(await signOrder(order, chainId, await swap.getAddress(), addr1)); diff --git a/test/helpers/orderUtils.js b/test/helpers/orderUtils.js index 3483d503..5dd6d840 100644 --- a/test/helpers/orderUtils.js +++ b/test/helpers/orderUtils.js @@ -124,6 +124,39 @@ function buildMakerTraits ({ ).toString(16).padStart(64, '0'); } +function buildFeeTakerExtensions({ + feeTaker, + feeRecipient, + makerReceiver = undefined, + integratorFee = 0, + resolverFee = 0, + whitelistDiscount = 50, + whitelistLength = 0, + whitelist = '0x', + whitelistPostInteraction = whitelist, +}) { + return { + makingAmountData: ethers.solidityPacked( + ['address', 'uint16', 'uint16', 'uint8', 'uint8', 'bytes'], + [feeTaker, integratorFee, resolverFee, whitelistDiscount, whitelistLength, whitelist], + ), + takingAmountData: ethers.solidityPacked( + ['address', 'uint16', 'uint16', 'uint8', 'uint8', 'bytes'], + [feeTaker, integratorFee, resolverFee, whitelistDiscount, whitelistLength, whitelist], + ), + postInteraction: ethers.solidityPacked( + ['address', 'bytes1', 'address'].concat( + makerReceiver ? ['address'] : [], + ['uint16', 'uint16', 'uint8', 'uint8', 'bytes'], + ), + [feeTaker, makerReceiver ? '0x01' : '0x00', feeRecipient].concat( + makerReceiver ? [makerReceiver] : [], + [integratorFee, resolverFee, whitelistDiscount, whitelistLength, whitelistPostInteraction], + ), + ), + } +} + function buildOrderRFQ ( { maker, @@ -276,6 +309,7 @@ module.exports = { buildTakerTraits, buildMakerTraits, buildMakerTraitsRFQ, + buildFeeTakerExtensions, buildOrder, buildOrderRFQ, buildOrderData, From 06b7d79cb666cf9568bc460be7c3fb1fda6ba9d1 Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Wed, 20 Nov 2024 14:07:31 +0400 Subject: [PATCH 06/17] lint --- test/helpers/orderUtils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/helpers/orderUtils.js b/test/helpers/orderUtils.js index 5dd6d840..866ab137 100644 --- a/test/helpers/orderUtils.js +++ b/test/helpers/orderUtils.js @@ -124,7 +124,7 @@ function buildMakerTraits ({ ).toString(16).padStart(64, '0'); } -function buildFeeTakerExtensions({ +function buildFeeTakerExtensions ({ feeTaker, feeRecipient, makerReceiver = undefined, @@ -154,7 +154,7 @@ function buildFeeTakerExtensions({ [integratorFee, resolverFee, whitelistDiscount, whitelistLength, whitelistPostInteraction], ), ), - } + }; } function buildOrderRFQ ( From f6ba911358206504cb690dcfdb323104b62e92a9 Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Wed, 20 Nov 2024 14:09:59 +0400 Subject: [PATCH 07/17] support extra prefix for getters --- test/helpers/orderUtils.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/helpers/orderUtils.js b/test/helpers/orderUtils.js index 866ab137..55a319f9 100644 --- a/test/helpers/orderUtils.js +++ b/test/helpers/orderUtils.js @@ -126,6 +126,7 @@ function buildMakerTraits ({ function buildFeeTakerExtensions ({ feeTaker, + getterExtraPrefix = '0x', feeRecipient, makerReceiver = undefined, integratorFee = 0, @@ -137,12 +138,12 @@ function buildFeeTakerExtensions ({ }) { return { makingAmountData: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'uint8', 'uint8', 'bytes'], - [feeTaker, integratorFee, resolverFee, whitelistDiscount, whitelistLength, whitelist], + ['address', 'bytes', 'uint16', 'uint16', 'uint8', 'uint8', 'bytes'], + [feeTaker, getterExtraPrefix, integratorFee, resolverFee, whitelistDiscount, whitelistLength, whitelist], ), takingAmountData: ethers.solidityPacked( - ['address', 'uint16', 'uint16', 'uint8', 'uint8', 'bytes'], - [feeTaker, integratorFee, resolverFee, whitelistDiscount, whitelistLength, whitelist], + ['address', 'bytes', 'uint16', 'uint16', 'uint8', 'uint8', 'bytes'], + [feeTaker, getterExtraPrefix, integratorFee, resolverFee, whitelistDiscount, whitelistLength, whitelist], ), postInteraction: ethers.solidityPacked( ['address', 'bytes1', 'address'].concat( From 9135a1ecd8b1cd848ac111a6add4afa3eb2867d3 Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Wed, 20 Nov 2024 14:14:32 +0400 Subject: [PATCH 08/17] add default feeRecipient --- test/FeeTaker.js | 14 +++++++------- test/helpers/orderUtils.js | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/FeeTaker.js b/test/FeeTaker.js index eb8b51a7..e96186f7 100644 --- a/test/FeeTaker.js +++ b/test/FeeTaker.js @@ -64,7 +64,7 @@ describe.only('FeeTaker', function () { 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]); + await expect(fillTx).to.changeTokenBalances(weth, [addr, addr1, feeRecipient], [-takingAmount, takingAmount, 0]); }); it('should send all tokens to the maker receiver with 0 fee', async function () { @@ -98,7 +98,7 @@ describe.only('FeeTaker', function () { 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]); + await expect(fillTx).to.changeTokenBalances(weth, [addr, addr1, feeRecipient, makerReceiver], [-takingAmount, 0, 0, takingAmount]); }); it('should charge fee when in whitelist', async function () { @@ -140,7 +140,7 @@ describe.only('FeeTaker', function () { const feeCalculated = takingAmount * (integratorFee + resolverFee / 2n) / 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]); + await expect(fillTx).to.changeTokenBalances(weth, [addr, addr1, feeRecipient], [-takingAmount - feeCalculated, takingAmount, feeCalculated]); }); it('should charge fee when out of whitelist', async function () { @@ -183,7 +183,7 @@ describe.only('FeeTaker', function () { 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], + [addr, addr1, feeRecipient], [-takingAmount - feeCalculated, takingAmount, feeCalculated], ); }); @@ -222,7 +222,7 @@ describe.only('FeeTaker', function () { 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 - feeCalculated, 0, feeCalculated, takingAmount]); + await expect(fillTx).to.changeTokenBalances(weth, [addr, addr1, feeRecipient, makerReceiver], [-takingAmount - feeCalculated, 0, feeCalculated, takingAmount]); }); it('should charge fee in eth', async function () { @@ -259,7 +259,7 @@ describe.only('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, addr2], [takingAmount, feeCalculated]); + await expect(fillTx).to.changeEtherBalances([addr1, feeRecipient], [takingAmount, feeCalculated]); }); it('should charge fee in eth and send the rest to the maker receiver', async function () { @@ -298,6 +298,6 @@ describe.only('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, addr2, addr3], [0, feeCalculated, takingAmount]); + await expect(fillTx).to.changeEtherBalances([addr1, feeRecipient, makerReceiver], [0, feeCalculated, takingAmount]); }); }); diff --git a/test/helpers/orderUtils.js b/test/helpers/orderUtils.js index 55a319f9..c29958fb 100644 --- a/test/helpers/orderUtils.js +++ b/test/helpers/orderUtils.js @@ -127,7 +127,7 @@ function buildMakerTraits ({ function buildFeeTakerExtensions ({ feeTaker, getterExtraPrefix = '0x', - feeRecipient, + feeRecipient = constants.ZERO_ADDRESS, makerReceiver = undefined, integratorFee = 0, resolverFee = 0, From 4081669a74afb03173d24222197027938939f5d3 Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Wed, 20 Nov 2024 16:41:57 +0400 Subject: [PATCH 09/17] fix --- test/FeeTaker.js | 8 +++----- test/helpers/orderUtils.js | 15 +++++++-------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/test/FeeTaker.js b/test/FeeTaker.js index e96186f7..3c6f32b7 100644 --- a/test/FeeTaker.js +++ b/test/FeeTaker.js @@ -6,7 +6,7 @@ const { deploySwapTokens } = require('./helpers/fixtures'); const { buildOrder, buildTakerTraits, signOrder, buildMakerTraits, buildFeeTakerExtensions } = require('./helpers/orderUtils'); const { ether } = require('./helpers/utils'); -describe.only('FeeTaker', function () { +describe('FeeTaker', function () { let addr, addr1, addr2, addr3; before(async function () { [addr, addr1, addr2, addr3] = await ethers.getSigners(); @@ -109,7 +109,7 @@ describe.only('FeeTaker', function () { const integratorFee = BigInt(1e4); const resolverFee = BigInt(1e3); const feeRecipient = addr2.address; - const whitelist = '0x' + addr.address.slice(-20).repeat(10); + const whitelist = '0x0a' + addr.address.slice(-20).repeat(10); const order = buildOrder( { @@ -125,7 +125,6 @@ describe.only('FeeTaker', function () { feeRecipient, integratorFee, resolverFee, - whitelistLength: 10, whitelist, }), ); @@ -151,7 +150,7 @@ describe.only('FeeTaker', function () { const integratorFee = BigInt(1e4); const resolverFee = BigInt(1e3); const feeRecipient = addr2.address; - const whitelist = '0x' + addr2.address.slice(-20).repeat(10); + const whitelist = '0x0a' + addr2.address.slice(-20).repeat(10); const order = buildOrder( { @@ -167,7 +166,6 @@ describe.only('FeeTaker', function () { feeRecipient, integratorFee, resolverFee, - whitelistLength: 10, whitelist, }), ); diff --git a/test/helpers/orderUtils.js b/test/helpers/orderUtils.js index c29958fb..5b574c5d 100644 --- a/test/helpers/orderUtils.js +++ b/test/helpers/orderUtils.js @@ -132,27 +132,26 @@ function buildFeeTakerExtensions ({ integratorFee = 0, resolverFee = 0, whitelistDiscount = 50, - whitelistLength = 0, - whitelist = '0x', + whitelist = '0x00', whitelistPostInteraction = whitelist, }) { return { makingAmountData: ethers.solidityPacked( - ['address', 'bytes', 'uint16', 'uint16', 'uint8', 'uint8', 'bytes'], - [feeTaker, getterExtraPrefix, integratorFee, resolverFee, whitelistDiscount, whitelistLength, whitelist], + ['address', 'bytes', 'uint16', 'uint16', 'uint8', 'bytes'], + [feeTaker, getterExtraPrefix, integratorFee, resolverFee, whitelistDiscount, whitelist], ), takingAmountData: ethers.solidityPacked( - ['address', 'bytes', 'uint16', 'uint16', 'uint8', 'uint8', 'bytes'], - [feeTaker, getterExtraPrefix, integratorFee, resolverFee, whitelistDiscount, whitelistLength, whitelist], + ['address', 'bytes', 'uint16', 'uint16', 'uint8', 'bytes'], + [feeTaker, getterExtraPrefix, integratorFee, resolverFee, whitelistDiscount, whitelist], ), postInteraction: ethers.solidityPacked( ['address', 'bytes1', 'address'].concat( makerReceiver ? ['address'] : [], - ['uint16', 'uint16', 'uint8', 'uint8', 'bytes'], + ['uint16', 'uint16', 'uint8', 'bytes'], ), [feeTaker, makerReceiver ? '0x01' : '0x00', feeRecipient].concat( makerReceiver ? [makerReceiver] : [], - [integratorFee, resolverFee, whitelistDiscount, whitelistLength, whitelistPostInteraction], + [integratorFee, resolverFee, whitelistDiscount, whitelistPostInteraction], ), ), }; From 11448df06adefc0e81e217c3c5b8cd830b4e90c1 Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Wed, 20 Nov 2024 16:54:24 +0400 Subject: [PATCH 10/17] allow bypassing FeeTaker --- contracts/extensions/FeeTaker.sol | 27 ++++++++++++++++++--------- test/FeeTaker.js | 14 +++----------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/contracts/extensions/FeeTaker.sol b/contracts/extensions/FeeTaker.sol index 18445a13..5d372d44 100644 --- a/contracts/extensions/FeeTaker.sol +++ b/contracts/extensions/FeeTaker.sol @@ -38,6 +38,11 @@ contract FeeTaker is IPostInteraction, AmountGetterWithFee, Ownable { */ error EthTransferFailed(); + /** + * @dev Fee taker is set to be receiver but no fees were set. + */ + error InconsistentFee(); + address private immutable _LIMIT_ORDER_PROTOCOL; address private immutable _WETH; /// @notice Contract address whose tokens allow filling limit orders with a fee for resolvers that are outside the whitelist @@ -128,16 +133,20 @@ contract FeeTaker is IPostInteraction, AmountGetterWithFee, Ownable { // 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); - 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); + if (order.receiver.get() == address(this)) { + if (fee == 0) revert InconsistentFee(); + + 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); + } + IERC20(order.takerAsset.get()).safeTransfer(receiver, takingAmount - fee); } - IERC20(order.takerAsset.get()).safeTransfer(receiver, takingAmount - fee); } if (tail.length >= 20) { diff --git a/test/FeeTaker.js b/test/FeeTaker.js index 3c6f32b7..c9d31cab 100644 --- a/test/FeeTaker.js +++ b/test/FeeTaker.js @@ -45,16 +45,12 @@ describe('FeeTaker', function () { const order = buildOrder( { maker: addr1.address, - receiver: await feeTaker.getAddress(), makerAsset: await dai.getAddress(), takerAsset: await weth.getAddress(), makingAmount, takingAmount, }, - buildFeeTakerExtensions({ - feeTaker: await feeTaker.getAddress(), - feeRecipient, - }), + buildFeeTakerExtensions({ feeTaker: await feeTaker.getAddress() }), ); const { r, yParityAndS: vs } = ethers.Signature.from(await signOrder(order, chainId, await swap.getAddress(), addr1)); @@ -78,17 +74,13 @@ describe('FeeTaker', function () { const order = buildOrder( { maker: addr1.address, - receiver: await feeTaker.getAddress(), + receiver: makerReceiver, makerAsset: await dai.getAddress(), takerAsset: await weth.getAddress(), makingAmount, takingAmount, }, - buildFeeTakerExtensions({ - feeTaker: await feeTaker.getAddress(), - feeRecipient, - makerReceiver, - }), + buildFeeTakerExtensions({ feeTaker: await feeTaker.getAddress() }), ); const { r, yParityAndS: vs } = ethers.Signature.from(await signOrder(order, chainId, await swap.getAddress(), addr1)); From 248d9d27593668c45a2159f3189fdd2cda7dba0a Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Wed, 20 Nov 2024 16:56:17 +0400 Subject: [PATCH 11/17] allow custom extra data --- test/helpers/orderUtils.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/helpers/orderUtils.js b/test/helpers/orderUtils.js index 5b574c5d..74ab54c3 100644 --- a/test/helpers/orderUtils.js +++ b/test/helpers/orderUtils.js @@ -134,24 +134,27 @@ function buildFeeTakerExtensions ({ whitelistDiscount = 50, whitelist = '0x00', whitelistPostInteraction = whitelist, + customMakingGetter = '0x', + customTakingGetter = '0x', + customPostInteraction = '0x', }) { return { makingAmountData: ethers.solidityPacked( - ['address', 'bytes', 'uint16', 'uint16', 'uint8', 'bytes'], - [feeTaker, getterExtraPrefix, integratorFee, resolverFee, whitelistDiscount, whitelist], + ['address', 'bytes', 'uint16', 'uint16', 'uint8', 'bytes', 'bytes'], + [feeTaker, getterExtraPrefix, integratorFee, resolverFee, whitelistDiscount, whitelist, customMakingGetter], ), takingAmountData: ethers.solidityPacked( - ['address', 'bytes', 'uint16', 'uint16', 'uint8', 'bytes'], - [feeTaker, getterExtraPrefix, integratorFee, resolverFee, whitelistDiscount, whitelist], + ['address', 'bytes', 'uint16', 'uint16', 'uint8', 'bytes', 'bytes'], + [feeTaker, getterExtraPrefix, integratorFee, resolverFee, whitelistDiscount, whitelist, customTakingGetter], ), postInteraction: ethers.solidityPacked( ['address', 'bytes1', 'address'].concat( makerReceiver ? ['address'] : [], - ['uint16', 'uint16', 'uint8', 'bytes'], + ['uint16', 'uint16', 'uint8', 'bytes', 'bytes'], ), [feeTaker, makerReceiver ? '0x01' : '0x00', feeRecipient].concat( makerReceiver ? [makerReceiver] : [], - [integratorFee, resolverFee, whitelistDiscount, whitelistPostInteraction], + [integratorFee, resolverFee, whitelistDiscount, whitelistPostInteraction, customPostInteraction], ), ), }; From d6b40b6e8fe579805eb70d925fb1044474eddcdd Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Wed, 20 Nov 2024 17:59:10 +0400 Subject: [PATCH 12/17] fix --- contracts/extensions/FeeTaker.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/extensions/FeeTaker.sol b/contracts/extensions/FeeTaker.sol index 5d372d44..a601b137 100644 --- a/contracts/extensions/FeeTaker.sol +++ b/contracts/extensions/FeeTaker.sol @@ -134,7 +134,7 @@ contract FeeTaker is IPostInteraction, AmountGetterWithFee, Ownable { uint256 fee = Math.mulDiv(takingAmount, integratorFee, denominator) + Math.mulDiv(takingAmount, resolverFee, denominator); if (order.receiver.get() == address(this)) { - if (fee == 0) revert InconsistentFee(); + if (integratorFee + resolverFee == 0) revert InconsistentFee(); if (order.takerAsset.get() == address(_WETH) && order.makerTraits.unwrapWeth()) { if (fee > 0) { From a27230667f09252a8e37c6c0b4f479919e26e607 Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Thu, 21 Nov 2024 00:09:57 +0400 Subject: [PATCH 13/17] fix --- contracts/extensions/FeeTaker.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/extensions/FeeTaker.sol b/contracts/extensions/FeeTaker.sol index a601b137..dcc097c7 100644 --- a/contracts/extensions/FeeTaker.sol +++ b/contracts/extensions/FeeTaker.sol @@ -134,8 +134,6 @@ contract FeeTaker is IPostInteraction, AmountGetterWithFee, Ownable { uint256 fee = Math.mulDiv(takingAmount, integratorFee, denominator) + Math.mulDiv(takingAmount, resolverFee, denominator); if (order.receiver.get() == address(this)) { - if (integratorFee + resolverFee == 0) revert InconsistentFee(); - if (order.takerAsset.get() == address(_WETH) && order.makerTraits.unwrapWeth()) { if (fee > 0) { _sendEth(feeRecipient, fee); From a38b830b93ee8164ac99c55ad916474b0857495f Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Thu, 21 Nov 2024 00:22:45 +0400 Subject: [PATCH 14/17] cleanup --- contracts/extensions/FeeTaker.sol | 5 ----- 1 file changed, 5 deletions(-) diff --git a/contracts/extensions/FeeTaker.sol b/contracts/extensions/FeeTaker.sol index dcc097c7..02eeeb9a 100644 --- a/contracts/extensions/FeeTaker.sol +++ b/contracts/extensions/FeeTaker.sol @@ -38,11 +38,6 @@ contract FeeTaker is IPostInteraction, AmountGetterWithFee, Ownable { */ error EthTransferFailed(); - /** - * @dev Fee taker is set to be receiver but no fees were set. - */ - error InconsistentFee(); - address private immutable _LIMIT_ORDER_PROTOCOL; address private immutable _WETH; /// @notice Contract address whose tokens allow filling limit orders with a fee for resolvers that are outside the whitelist From 5e7ca6409d9d111ccbc8c1b7e186eb5f99bb9429 Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Tue, 26 Nov 2024 12:56:17 +0400 Subject: [PATCH 15/17] fix typo --- contracts/extensions/AmountGetterWithFee.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/extensions/AmountGetterWithFee.sol b/contracts/extensions/AmountGetterWithFee.sol index b8f283e5..9c726ca4 100644 --- a/contracts/extensions/AmountGetterWithFee.sol +++ b/contracts/extensions/AmountGetterWithFee.sol @@ -88,7 +88,7 @@ contract AmountGetterWithFee is AmountGetterBase { * @param whitelistData Whitelist data is a tightly packed struct of the following format: * ``` * 1 byte - size of the whitelist - * (bytes10)[N] whiteliested addresses; + * (bytes10)[N] whitelisted addresses; * ``` * Only 10 lowest bytes of the address are used for comparison. * @param taker The taker address to check. From 34234a3a877712842db3ea77a3cb1850bd7a1290 Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Tue, 26 Nov 2024 13:30:00 +0400 Subject: [PATCH 16/17] revert when fee > 0 but receiver is not FeeTaker --- contracts/extensions/FeeTaker.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/contracts/extensions/FeeTaker.sol b/contracts/extensions/FeeTaker.sol index 02eeeb9a..8d9a71e9 100644 --- a/contracts/extensions/FeeTaker.sol +++ b/contracts/extensions/FeeTaker.sol @@ -38,6 +38,11 @@ contract FeeTaker is IPostInteraction, AmountGetterWithFee, Ownable { */ error EthTransferFailed(); + /** + * @dev Fees are specified but FeeTaker is not set as a receiver. + */ + error InconsistentFee(); + address private immutable _LIMIT_ORDER_PROTOCOL; address private immutable _WETH; /// @notice Contract address whose tokens allow filling limit orders with a fee for resolvers that are outside the whitelist @@ -140,6 +145,8 @@ contract FeeTaker is IPostInteraction, AmountGetterWithFee, Ownable { } IERC20(order.takerAsset.get()).safeTransfer(receiver, takingAmount - fee); } + } else if (fee > 0) { + revert InconsistentFee(); } if (tail.length >= 20) { From 994e529681f487b22ee14d9e52572f9d02ea862a Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Tue, 10 Dec 2024 14:55:14 +0400 Subject: [PATCH 17/17] add natspec Co-authored-by: Xenia <94478708+byshape@users.noreply.github.com> --- contracts/extensions/FeeTaker.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/extensions/FeeTaker.sol b/contracts/extensions/FeeTaker.sol index 8d9a71e9..e808ce06 100644 --- a/contracts/extensions/FeeTaker.sol +++ b/contracts/extensions/FeeTaker.sol @@ -72,6 +72,9 @@ contract FeeTaker is IPostInteraction, AmountGetterWithFee, Ownable { */ receive() external payable {} + /** + * @notice See {IPostInteraction-postInteraction}. + */ function postInteraction( IOrderMixin.Order calldata order, bytes calldata extension,