Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SC-1198] FeeTaker flow-3 (full calldata) #328

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions contracts/extensions/FeeBank.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.23;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@1inch/solidity-utils/contracts/libraries/SafeERC20.sol";
import { UniERC20 } from "@1inch/solidity-utils/contracts/libraries/UniERC20.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { FeeBankCharger } from "./FeeBankCharger.sol";

contract FeeBank is Ownable {
using SafeERC20 for IERC20;
using UniERC20 for IERC20;

error ZeroAddress();

IERC20 private immutable _FEE_TOKEN;
FeeBankCharger private immutable _CHARGER;

mapping(address account => uint256 availableCredit) private _accountDeposits;
mapping(address => bool) public payWithFeeBank;

constructor(FeeBankCharger charger, IERC20 feeToken, address owner) Ownable(owner) {
if (address(feeToken) == address(0)) revert ZeroAddress();
_CHARGER = charger;
_FEE_TOKEN = feeToken;
}

function setPayWithFeeBank(bool value) external {
payWithFeeBank[msg.sender] = value;
}

/**
* @notice See {IFeeBank-availableCredit}.
*/
function availableCredit(address account) external view returns (uint256) {
return _CHARGER.availableCredit(account);
}

/**
* @notice See {IFeeBank-deposit}.
*/
function deposit(uint256 amount) external returns (uint256) {
return _depositFor(msg.sender, amount);
}

/**
* @notice See {IFeeBank-depositFor}.
*/
function depositFor(address account, uint256 amount) external returns (uint256) {
return _depositFor(account, amount);
}

/**
* @notice See {IFeeBank-depositWithPermit}.
*/
function depositWithPermit(uint256 amount, bytes calldata permit) external returns (uint256) {
return depositForWithPermit(msg.sender, amount, permit);
}

/**
* @notice See {IFeeBank-depositForWithPermit}.
*/
function depositForWithPermit(
address account,
uint256 amount,
bytes calldata permit
) public returns (uint256) {
_FEE_TOKEN.safePermit(permit);
return _depositFor(account, amount);
}

/**
* @notice See {IFeeBank-withdraw}.
*/
function withdraw(uint256 amount) external returns (uint256) {
return _withdrawTo(msg.sender, amount);
}

/**
* @notice See {IFeeBank-withdrawTo}.
*/
function withdrawTo(address account, uint256 amount) external returns (uint256) {
return _withdrawTo(account, amount);
}

/**
* @notice Admin method returns commissions spent by users.
* @param accounts Accounts whose commissions are being withdrawn.
* @return totalAccountFees The total amount of accounts commissions.
*/
function gatherFees(address[] calldata accounts) external onlyOwner returns (uint256 totalAccountFees) {
uint256 accountsLength = accounts.length;
unchecked {
for (uint256 i = 0; i < accountsLength; ++i) {
address account = accounts[i];
uint256 accountDeposit = _accountDeposits[account];
uint256 availableCredit_ = _CHARGER.availableCredit(account);
_accountDeposits[account] = availableCredit_;
totalAccountFees += accountDeposit - availableCredit_; // overflow is impossible due to checks in FeeBankCharger
}
}
_FEE_TOKEN.safeTransfer(msg.sender, totalAccountFees);
}

function _depositFor(address account, uint256 amount) internal returns (uint256 totalAvailableCredit) {
if (account == address(0)) revert ZeroAddress();
_FEE_TOKEN.safeTransferFrom(msg.sender, address(this), amount);
unchecked {
_accountDeposits[account] += amount; // overflow is impossible due to limited _FEE_TOKEN supply
}
totalAvailableCredit = _CHARGER.increaseAvailableCredit(account, amount);
}

function _withdrawTo(address account, uint256 amount) internal returns (uint256 totalAvailableCredit) {
totalAvailableCredit = _CHARGER.decreaseAvailableCredit(msg.sender, amount);
unchecked {
_accountDeposits[msg.sender] -= amount; // underflow is impossible due to checks in FeeBankCharger
}
_FEE_TOKEN.safeTransfer(account, amount);
}

/**
* @notice Retrieves funds accidently sent directly to the contract address
* @param token ERC20 token to retrieve
* @param amount amount to retrieve
*/
function rescueFunds(IERC20 token, uint256 amount) external onlyOwner {
token.uniTransfer(payable(msg.sender), amount);
}
}
74 changes: 74 additions & 0 deletions contracts/extensions/FeeBankCharger.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.23;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { FeeBank } from "./FeeBank.sol";

/**
* @title FeeBankCharger
* @notice FeeBankCharger contract implements logic to increase or decrease users' credits in FeeBank.
*/
contract FeeBankCharger {
error OnlyFeeBankAccess();
error NotEnoughCredit();

/**
* @notice See {IFeeBankCharger-feeBank}.
*/
address public immutable FEE_BANK;
mapping(address => uint256) private _creditAllowance;

/**
* @dev Modifier to check if the sender is a FEE_BANK contract.
*/
modifier onlyFeeBank() {
if (msg.sender != FEE_BANK) revert OnlyFeeBankAccess();
_;
}

constructor(IERC20 feeToken, address owner) {
FEE_BANK = address(new FeeBank(this, feeToken, owner));
}

/**
* @notice See {IFeeBankCharger-availableCredit}.
*/
function availableCredit(address account) external view returns (uint256) {
return _creditAllowance[account];
}

/**
* @notice See {IFeeBankCharger-increaseAvailableCredit}.
*/
function increaseAvailableCredit(address account, uint256 amount) external onlyFeeBank returns (uint256 allowance) {
allowance = _creditAllowance[account];
unchecked {
allowance += amount; // overflow is impossible due to limited _token supply
}
_creditAllowance[account] = allowance;
}

/**
* @notice See {IFeeBankCharger-decreaseAvailableCredit}.
*/
function decreaseAvailableCredit(address account, uint256 amount) external onlyFeeBank returns (uint256 allowance) {
return _creditAllowance[account] -= amount; // checked math is needed to prevent underflow
}

/**
* @notice Internal function that charges a specified fee from a given account's credit allowance.
* @dev Reverts with 'NotEnoughCredit' if the account's credit allowance is insufficient to cover the fee.
* @param account The address of the account from which the fee is being charged.
* @param fee The amount of fee to be charged from the account.
*/
function _chargeFee(address account, uint256 fee) internal virtual {
if (fee > 0) {
uint256 currentAllowance = _creditAllowance[account];
if (currentAllowance < fee) revert NotEnoughCredit();
unchecked {
_creditAllowance[account] = currentAllowance - fee;
}
}
}
}
105 changes: 81 additions & 24 deletions contracts/extensions/FeeTaker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ 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 { IOrderMixin } from "../interfaces/IOrderMixin.sol";
import { IPostInteraction } from "../interfaces/IPostInteraction.sol";
import { MakerTraits, MakerTraitsLib } from "../libraries/MakerTraitsLib.sol";
import { FeeBank } from "./FeeBank.sol";
import { FeeBankCharger } from "./FeeBankCharger.sol";

/// @title Helper contract that adds feature of collecting fee in takerAsset
contract FeeTaker is IPostInteraction, Ownable {
contract FeeTaker is IPostInteraction, FeeBankCharger, Ownable {
using AddressLib for Address;
using SafeERC20 for IERC20;
using UniERC20 for IERC20;
Expand Down Expand Up @@ -45,7 +48,7 @@ contract FeeTaker is IPostInteraction, Ownable {
* @notice Initializes the contract.
* @param limitOrderProtocol The limit order protocol contract.
*/
constructor(address limitOrderProtocol, address weth, address owner) Ownable(owner) {
constructor(address limitOrderProtocol, address weth, IERC20 feeToken, address owner) FeeBankCharger(feeToken, owner) Ownable(owner) {
_LIMIT_ORDER_PROTOCOL = limitOrderProtocol;
_WETH = weth;
}
Expand All @@ -59,43 +62,72 @@ contract FeeTaker is IPostInteraction, Ownable {
* @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)
* 20 bytes — fee recipient
* 16 bytes - feeBank flat fee
* 1 byte - taker whitelist size
* (bytes10)[N] — taker whitelist
* 20 bytes — receiver of taking tokens (optional, if not set, maker is used)
*/
function postInteraction(
IOrderMixin.Order calldata order,
bytes calldata /* extension */,
bytes32 /* orderHash */,
address /* taker */,
uint256 /* makingAmount */,
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();
unchecked {
uint256 integratorFee;
uint256 resolverFee;
{
uint256 integratorFeePercent = uint16(bytes2(extraData));
uint256 resolverFeePercent = uint16(bytes2(extraData[2:]));
uint256 denominator = _FEE_BASE + integratorFeePercent + 2 * resolverFeePercent;
integratorFee = Math.mulDiv(takingAmount, integratorFeePercent, denominator);
resolverFee = Math.mulDiv(takingAmount, resolverFeePercent, denominator);
}

if (isEth) {
if (fee > 0) {
_sendEth(feeRecipient, fee);
address feeRecipient = address(bytes20(extraData[4:24]));
uint256 feeBankFee = uint128(bytes16(extraData[24:40]));
uint256 whitelistEnd = 41 + uint8(extraData[40]) * 10;
uint256 maxFee = integratorFee + 2 * resolverFee;
uint256 fee = maxFee;
uint256 cashback;
if (_isWhitelisted(extraData[41:whitelistEnd], taker)) {
fee -= resolverFee;
cashback = resolverFee;
}
unchecked {
_sendEth(receiver, takingAmount - fee);
if (FeeBank(FEE_BANK).payWithFeeBank(taker)) {
_chargeFee(taker, Math.mulDiv(feeBankFee, makingAmount, order.makingAmount));
cashback = maxFee;
fee = 0;
}
} else {
if (fee > 0) {
IERC20(order.takerAsset.get()).safeTransfer(feeRecipient, fee);

address receiver = order.maker.get();
if (extraData.length > whitelistEnd) {
receiver = address(bytes20(extraData[whitelistEnd:whitelistEnd + 20]));
}
unchecked {
IERC20(order.takerAsset.get()).safeTransfer(receiver, takingAmount - fee);

if (order.takerAsset.get() == address(_WETH) && order.makerTraits.unwrapWeth()) {
if (fee > 0) {
_sendEth(feeRecipient, fee);
}
if (cashback > 0) {
_sendEth(taker, cashback);
}
_sendEth(receiver, takingAmount - fee - cashback);
} else {
if (fee > 0) {
IERC20(order.takerAsset.get()).safeTransfer(feeRecipient, fee);
}
if (cashback > 0) {
IERC20(order.takerAsset.get()).safeTransfer(taker, cashback);
}
IERC20(order.takerAsset.get()).safeTransfer(receiver, takingAmount - fee - cashback);
}
}
}
Expand All @@ -115,4 +147,29 @@ 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;
}
}
}
Loading
Loading