Skip to content

Commit

Permalink
feat: Add client take rate
Browse files Browse the repository at this point in the history
  • Loading branch information
cucupac committed Jan 29, 2024
1 parent 3d31687 commit 13ea2ef
Show file tree
Hide file tree
Showing 9 changed files with 108 additions and 25 deletions.
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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🙂
36 changes: 35 additions & 1 deletion src/FeeCollector.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.
*/
Expand Down Expand Up @@ -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.
Expand Down
15 changes: 4 additions & 11 deletions src/Position.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand All @@ -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);
Expand All @@ -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.
Expand Down
22 changes: 21 additions & 1 deletion src/interfaces/IFeeCollector.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/interfaces/IPosition.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion src/interfaces/IPositionFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
29 changes: 29 additions & 0 deletions src/libraries/FeeLib.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 1 addition & 1 deletion test/common/Constants.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions test/integration/FeeCollector.short.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
CONTRACT_DEPLOYER,
FEE_COLLECTOR,
TEST_CLIENT,
PROTOCOL_FEE,
PROTOCOL_FEE_RATE,
CLIENT_RATE,
USDC,
WETH,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 13ea2ef

Please sign in to comment.