diff --git a/README.md b/README.md index ee58e956..23bc6019 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,19 @@ The following outlines principles for core protocol funcitonality. Logic: -- All caught up!🙂 +- Move PositionAdmin to services, rename +- [ ] Consider changing short() to add() +- [ ] Emit event when a position is created (get clear on whether or not an implicit event is emitted when creating a contract) Tests: -- [x] Separate integration tests from unit tests (separate PR) -- [x] Move all utils to test/common/ +- [ ] Invariant: the clientTakeRate + userTakeRate = clientRate +- [ ] Invariant: the totalTokenAmt - sum(clientFeesToken) = (1 - clientRate) \* totalTokenAmt +- [ ] Unit test setClientTakeRate() +- [ ] Unit test getUserSavings() +- [ ] Unit test FeeLib via Test Harness +- [ ] Account for userSavings in all affected FeeCollector unit and integration tests Considerations: -- [ ] Consider emitting Position events through another contract -- [ ] Consider adding a function to short with signatures, via `ERCRecover`. +- None at the moment🙂 diff --git a/src/FeeCollector.sol b/src/FeeCollector.sol index d97cc8ec..4f1b033e 100644 --- a/src/FeeCollector.sol +++ b/src/FeeCollector.sol @@ -15,6 +15,7 @@ contract FeeCollector is Ownable { // Storage uint256 public clientRate; + mapping(address => uint256) public clientTakeRates; mapping(address => uint256) public totalClientBalances; mapping(address => mapping(address => uint256)) public balances; @@ -28,7 +29,7 @@ contract FeeCollector is Ownable { /** * @notice Collects fees from Position contracts when collateral is added. - * @param _client The address, controlled by client operators, for receiving protocol fees. + * @param _client The address where a client operator will receive protocols fees. * @param _token The token to collect fees in (the collateral token of the calling Position contract). * @param _amt The total amount of fees to collect. */ @@ -59,6 +60,39 @@ contract FeeCollector is Ownable { SafeTransferLib.safeTransfer(ERC20(_token), msg.sender, withdrawAmt); } + /** + * @notice Allows clients to set the percentage of the clientRate they will receive each revenue-generating tx. + * Amounts less than 100 will give the calling client's users a protocol fee discount: + * clientTakeRateOfProtocolFee = clientRate * _clientTakeRate + * ex: _clientTakeRate = 50% -> clientTakeRate = clientRate * 0.5 + * userTakeRateOfProtocolFee = clientRate * (1 - _clientTakeRate) + * ex: _clientTakeRate = 50% -> userTakeRate = clientRate * (1 - 0.5) + * clientFee = protocolFee * clientTakeRateOfProtocolFee + * userSavings = protocolFee * userTakeRateOfProtocolFee + * @param _clientTakeRate The percentage of the clientRate the client will receive each revenue-generating tx (100 = 100%). + */ + function setClientTakeRate(uint256 _clientTakeRate) public payable { + if (_clientTakeRate > 100) revert OutOfRange(); + clientTakeRates[msg.sender] = _clientTakeRate; + } + + /** + * @notice Returns the amount discounted from the protocol fee by using the provided client. + * @param _client The address where a client operator will receive protocols fees. + * @param _maxFee The maximum amount of fees the protocol will collect. + */ + function getUserSavings(address _client, uint256 _maxFee) public view returns (uint256 userSavings) { + uint256 userTakeRate = 100 - clientTakeRates[_client]; + uint256 userPercentOfProtocolFee = (userTakeRate * clientRate) / 100; + userSavings = (userPercentOfProtocolFee * _maxFee) / 100; + } + + /* **************************************************************************** + ** + ** Admin Functions + ** + ******************************************************************************/ + /** * @notice Allows owner to set client rate. * @param _clientRate The percentage of total transaction-specific protocol fee, allocated to the utilized client. diff --git a/src/Position.sol b/src/Position.sol index ad4e43f8..f6ddbbf1 100644 --- a/src/Position.sol +++ b/src/Position.sol @@ -5,18 +5,14 @@ pragma solidity ^0.8.21; import { DebtService } from "src/services/DebtService.sol"; import { SwapService } from "src/services/SwapService.sol"; import { SafeTransferLib, ERC20 } from "solmate/utils/SafeTransferLib.sol"; +import { FeeLib } from "src/libraries/FeeLib.sol"; import { IERC20 } from "src/interfaces/token/IERC20.sol"; import { IERC20Permit } from "src/interfaces/token/IERC20Permit.sol"; -import { IFeeCollector } from "src/interfaces/IFeeCollector.sol"; /// @title Position /// @author Chain Rule, LLC /// @notice Manages the owner's individual position contract Position is DebtService, SwapService { - // Constants: no SLOAD to save gas - uint256 public constant PROTOCOL_FEE = 3; - address public constant FEE_COLLECTOR = 0x7A7AbDb9E12F3a9845E2433958Eef8FB9C8489Ee; - // Immutables: no SLOAD to save gas address public immutable B_TOKEN; @@ -36,7 +32,7 @@ contract Position is DebtService, SwapService { * @param _ltv The desired loan-to-value ratio for this transaction-specific loan (ex: 75 is 75%). * @param _swapAmtOutMin The minimum amount of output tokens from swap for the tx to go through. * @param _poolFee The fee of the Uniswap pool. - * @param _client The address, controlled by client operators, for receiving protocol fees (use address(0) if no client). + * @param _client The address where a client operator will receive protocols fees. (use address(0) if no client). */ function short(uint256 _cAmt, uint256 _ltv, uint256 _swapAmtOutMin, uint24 _poolFee, address _client) public @@ -47,10 +43,7 @@ contract Position is DebtService, SwapService { SafeTransferLib.safeTransferFrom(ERC20(C_TOKEN), msg.sender, address(this), _cAmt); // 2. Take protocol fee - uint256 protocolFee = (_cAmt * PROTOCOL_FEE) / 1000; - uint256 cAmtNet = _cAmt - protocolFee; - SafeTransferLib.safeApprove(ERC20(C_TOKEN), FEE_COLLECTOR, protocolFee); - IFeeCollector(FEE_COLLECTOR).collectFees(_client, C_TOKEN, protocolFee); + uint256 cAmtNet = FeeLib.takeProtocolFee(C_TOKEN, _cAmt, _client); // 3. Borrow debt token uint256 dAmt = _borrow(cAmtNet, _ltv); @@ -68,7 +61,7 @@ contract Position is DebtService, SwapService { * @param _ltv The desired loan-to-value ratio for this transaction-specific loan (ex: 75 is 75%). * @param _swapAmtOutMin The minimum amount of output tokens from swap for the tx to go through. * @param _poolFee The fee of the Uniswap pool. - * @param _client The address, controlled by client operators, for receiving protocol fees (use address(0) if no client). + * @param _client The address where a client operator will receive protocols fees. (use address(0) if no client). * @param _deadline The deadline timestamp that the permit is valid. * @param _v The V parameter of ERC712 permit signature. * @param _r The R parameter of ERC712 permit signature. diff --git a/src/interfaces/IFeeCollector.sol b/src/interfaces/IFeeCollector.sol index 913bcfc7..b8397ac4 100644 --- a/src/interfaces/IFeeCollector.sol +++ b/src/interfaces/IFeeCollector.sol @@ -35,7 +35,7 @@ interface IFeeCollector { /** * @notice Collects fees from Position contracts when collateral is added. - * @param _client The address, controlled by client operators, for receiving protocol fees. + * @param _client The address where a client operator will receive protocols fees. * @param _token The token to collect fees in (the collateral token of the calling Position contract). * @param _amt The total amount of fees to collect. */ @@ -46,6 +46,26 @@ interface IFeeCollector { */ function clientWithdraw(address _token) external payable; + /** + * @notice Allows clients to set the percentage of the clientRate they will receive each revenue-generating tx. + * Amounts less than 100 will give the calling client's users a protocol fee discount: + * clientTakeRateOfProtocolFee = clientRate * _clientTakeRate + * ex: _clientTakeRate = 50% -> clientTakeRate = clientRate * 0.5 + * userTakeRateOfProtocolFee = clientRate * (1 - _clientTakeRate) + * ex: _clientTakeRate = 50% -> userTakeRate = clientRate * (1 - 0.5) + * clientFee = protocolFee * clientTakeRateOfProtocolFee + * userSavings = protocolFee * userTakeRateOfProtocolFee + * @param _clientTakeRate The percentage of the clientRate the client will receive each revenue-generating tx (100 = 100%). + */ + function setClientTakeRate(uint256 _clientTakeRate) external payable; + + /** + * @notice Returns the amount discounted from the protocol fee by using the provided client. + * @param _client The address where a client operator will receive protocols fees. + * @param _protocolFee The maximum amount of fees the protocol will collect. + */ + function getUserSavings(address _client, uint256 _protocolFee) external view returns (uint256 userSavings); + /* **************************************************************************** ** ** ADMIN FUNCTIONS diff --git a/src/interfaces/IPosition.sol b/src/interfaces/IPosition.sol index d37eed73..29763478 100644 --- a/src/interfaces/IPosition.sol +++ b/src/interfaces/IPosition.sol @@ -36,7 +36,7 @@ interface IPosition { * @param _ltv The desired loan-to-value ratio for this transaction-specific loan (ex: 75 is 75%). * @param _swapAmtOutMin The minimum amount of output tokens from swap for the tx to go through. * @param _poolFee The fee of the Uniswap pool. - * @param _client The address, controlled by client operators, for receiving protocol fees (use address(0) if no client). + * @param _client The address where a client operator will receive protocols fees. (use address(0) if no client). */ function short(uint256 _cAmt, uint256 _ltv, uint256 _swapAmtOutMin, uint24 _poolFee, address _client) external @@ -48,7 +48,7 @@ interface IPosition { * @param _ltv The desired loan-to-value ratio for this transaction-specific loan (ex: 75 is 75%). * @param _swapAmtOutMin The minimum amount of output tokens from swap for the tx to go through. * @param _poolFee The fee of the Uniswap pool. - * @param _client The address, controlled by client operators, for receiving protocol fees (use address(0) if no client). + * @param _client The address where a client operator will receive protocols fees. (use address(0) if no client). * @param _deadline The deadline timestamp that the permit is valid. * @param _v The V parameter of ERC712 permit signature. * @param _r The R parameter of ERC712 permit signature. diff --git a/src/interfaces/IPositionFactory.sol b/src/interfaces/IPositionFactory.sol index 67a15846..884c70b8 100644 --- a/src/interfaces/IPositionFactory.sol +++ b/src/interfaces/IPositionFactory.sol @@ -23,7 +23,9 @@ interface IPositionFactory { returns (address); /** - * @notice Returns a list of contract addresses for the given owner. + * @notice Returns an indexed contract addresses from the list of contracts for the given owner. + * Direct external calls to this mapping require an index to retrieve + * a specific address. To get the full array, call getPositions(). */ function positionsLookup(address _owner) external view returns (address[] memory); diff --git a/src/libraries/FeeLib.sol b/src/libraries/FeeLib.sol new file mode 100644 index 00000000..35a73f2f --- /dev/null +++ b/src/libraries/FeeLib.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import { SafeTransferLib, ERC20 } from "solmate/utils/SafeTransferLib.sol"; +import { IFeeCollector } from "src/interfaces/IFeeCollector.sol"; + +/// @title FeeLib +/// @author Chain Rule, LLC +/// @notice Manages all protocol-fee-related interactions +library FeeLib { + // Constants: no SLOAD to save gas + uint256 public constant PROTOCOL_FEE_RATE = 3; + address public constant FEE_COLLECTOR = 0x7A7AbDb9E12F3a9845E2433958Eef8FB9C8489Ee; + + /** + * @notice Takes protocol fee from the amount of collateral supplied. + * @param _cAmt The amount of collateral to be supplied (units: C_DECIMALS). + * @param _client The address where a client operator will receive protocols fees. + * @return cAmtNet The resulting amount of collateral to be supplied after fees are taken. + */ + function takeProtocolFee(address _token, uint256 _cAmt, address _client) internal returns (uint256 cAmtNet) { + uint256 maxFee = (_cAmt * PROTOCOL_FEE_RATE) / 1000; + uint256 userSavings = IFeeCollector(FEE_COLLECTOR).getUserSavings(_client, maxFee); + uint256 fee = maxFee - userSavings; + cAmtNet = _cAmt - fee; + SafeTransferLib.safeApprove(ERC20(_token), FEE_COLLECTOR, fee); + IFeeCollector(FEE_COLLECTOR).collectFees(_client, _token, fee); + } +} diff --git a/test/common/Constants.t.sol b/test/common/Constants.t.sol index b5bee860..cdccda9f 100644 --- a/test/common/Constants.t.sol +++ b/test/common/Constants.t.sol @@ -23,7 +23,7 @@ uint256 constant PROFIT_PERCENT = 25; uint256 constant REPAY_PERCENT = 75; uint256 constant WITHDRAW_BUFFER = 100_000; uint256 constant REPAY_BUFFER = 2; -uint256 constant PROTOCOL_FEE = 3; +uint256 constant PROTOCOL_FEE_RATE = 3; uint256 constant CLIENT_RATE = 30; contract Assets { diff --git a/test/integration/FeeCollector.short.t.sol b/test/integration/FeeCollector.short.t.sol index 6321e063..d87651ed 100644 --- a/test/integration/FeeCollector.short.t.sol +++ b/test/integration/FeeCollector.short.t.sol @@ -13,7 +13,7 @@ import { CONTRACT_DEPLOYER, FEE_COLLECTOR, TEST_CLIENT, - PROTOCOL_FEE, + PROTOCOL_FEE_RATE, CLIENT_RATE, USDC, WETH, @@ -110,7 +110,7 @@ contract FeeCollectorShortTest is Test, TokenUtils { _cAmt = bound(_cAmt, assets.minCAmts(cToken), assets.maxCAmts(cToken)); // Expectations - uint256 protocolFee = (_cAmt * PROTOCOL_FEE) / 1000; + uint256 protocolFee = (_cAmt * PROTOCOL_FEE_RATE) / 1000; uint256 clientFee = (protocolFee * CLIENT_RATE) / 100; // Fund positionOwner with _cAmt of cToken @@ -153,7 +153,7 @@ contract FeeCollectorShortTest is Test, TokenUtils { _cAmt = bound(_cAmt, assets.minCAmts(cToken), assets.maxCAmts(cToken)); // Expectations - uint256 protocolFee = (_cAmt * PROTOCOL_FEE) / 1000; + uint256 protocolFee = (_cAmt * PROTOCOL_FEE_RATE) / 1000; // Fund positionOwner with _cAmt of cToken _fund(positionOwner, cToken, _cAmt);