Skip to content

Commit

Permalink
feat: add protocol fee (#7)
Browse files Browse the repository at this point in the history
* feat: add protocol fee

* docs: update README with todo

* chore: address requested changes
  • Loading branch information
cucupac authored Jan 24, 2024
1 parent 75cb729 commit ed5961b
Show file tree
Hide file tree
Showing 9 changed files with 482 additions and 25 deletions.
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,27 @@ On-chain shorting via Aave and Uniswap.

## To-Do

- [ ] Impelemnt protocol fee
- [ ] Implement frontend incentive
- [x] Impelemnt protocol fee
- [x] Implement frontend incentive
- [x] Account for case where there is no client (if no frontend, they would be able to pass their own address...)
- [x] Should not repay more to Aave than what is owed when closing a position

Unit Tests:

- [x] Mock feel collector in tests
- [x] Should not repay more to Aave than what is owed when closing a position
- [ ] Test that the client's collected fee balance increased by the correct amount
- [ ] Test that the totalClientBalance increased by the correct amount
- [ ] Test that an admin you can set a clientRate
- [ ] Test that a non-admin cannot set a clientRate
- [ ] Test that a client can withdraw their collected fees
- [ ] Test that extractERC20 works correctly on FeeCollector (it has different logic than the other contracts)
- [ ] Test that a non-admin cannot extractERC20
- [ ] Test that an admin can withdraw native
- [ ] Test that a non-admin cannot withdraw native
- [ ] Test that FeeCollector can recieve native
- [ ] Test Fallback on FeeCollector
- [ ] Test that the correct protocol fee is collected during a short (proabably separate test from Position.t.sol)

Considerations:

Expand Down
98 changes: 98 additions & 0 deletions src/FeeCollector.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

// Local Imports
import { Ownable } from "src/dependencies/access/Ownable.sol";
import { SafeTransferLib, ERC20 } from "solmate/utils/SafeTransferLib.sol";
import { IERC20 } from "src/interfaces/token/IERC20.sol";

/// @title FeeCollector
/// @author Chain Rule, LLC
/// @notice Collects protocol fees
contract FeeCollector is Ownable {
// Constants: no SLOAD to save gas
address private constant CONTRACT_DEPLOYER = 0x0a5B347509621337cDDf44CBCf6B6E7C9C908CD2;

// Storage
uint256 public clientRate;
mapping(address => uint256) public totalClientBalances;
mapping(address => mapping(address => uint256)) public balances;

// Errors
error Unauthorized();
error OutOfRange();

constructor(address _owner) Ownable(_owner) {
if (msg.sender != CONTRACT_DEPLOYER) revert Unauthorized();
}

/**
* @notice Collects fees from Position contracts when collateral is added.
* @param _client The address, controlled by client operators, for receiving protocol 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.
*/
function collectFees(address _client, address _token, uint256 _amt) external payable {
// 1. Transfer tokens to this contract
SafeTransferLib.safeTransferFrom(ERC20(_token), msg.sender, address(this), _amt);

// 2. Update client balances
if (_client != address(0)) {
uint256 clientFee = (_amt * clientRate) / 100;
balances[_client][_token] += clientFee;
totalClientBalances[_token] += clientFee;
}
}

/**
* @notice Withdraw collected fees from this contract.
* @param _token The token address to withdraw.
*/
function clientWithdraw(address _token) public payable {
uint256 withdrawAmt = balances[msg.sender][_token];

// 1. Update accounting
balances[msg.sender][_token] -= withdrawAmt;
totalClientBalances[_token] -= withdrawAmt;

// 2. Transfer tokens to msg.sender
SafeTransferLib.safeTransfer(ERC20(_token), msg.sender, withdrawAmt);
}

/**
* @notice Allows owner to set client rate.
* @param _clientRate The percentage of total transaction-specific protocol fee, allocated to the utilized client.
*/
function setClientRate(uint256 _clientRate) public payable onlyOwner {
if (_clientRate < 30 || _clientRate > 100) revert OutOfRange();

clientRate = _clientRate;
}

/**
* @notice Allows OWNER to withdraw all of this contract's native token balance.
*/
function extractNative() public payable onlyOwner {
payable(msg.sender).transfer(address(this).balance);
}

/**
* @notice Allows owner to withdraw protocol fees from this contract.
* @param _token The address of token to remove.
*/
function extractERC20(address _token) public payable onlyOwner {
uint256 withdrawAmt = IERC20(_token).balanceOf(address(this)) - totalClientBalances[_token];

SafeTransferLib.safeTransfer(ERC20(_token), msg.sender, withdrawAmt);
}

/**
* @notice Executes when native is sent to this contract through a non-existent function.
*/
fallback() external payable { } // solhint-disable-line no-empty-blocks

/**
* @notice Executes when native is sent to this contract with a plain transaction.
*/
receive() external payable { } // solhint-disable-line no-empty-blocks
}
28 changes: 22 additions & 6 deletions src/Position.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ import { DebtService } from "src/services/DebtService.sol";
import { SwapService } from "src/services/SwapService.sol";
import { SafeTransferLib, ERC20 } from "solmate/utils/SafeTransferLib.sol";
import { IERC20 } from "src/interfaces/token/IERC20.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 private constant FEE_COLLECTOR = 0x2cD6D948263F20C3c27f181f14647840fC64b488;

// Immutables: no SLOAD to save gas
address public immutable B_TOKEN;

Expand All @@ -30,19 +35,30 @@ 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).
*/
function short(uint256 _cAmt, uint256 _ltv, uint256 _swapAmtOutMin, uint24 _poolFee) public payable onlyOwner {
function short(uint256 _cAmt, uint256 _ltv, uint256 _swapAmtOutMin, uint24 _poolFee, address _client)
public
payable
onlyOwner
{
// 1. Transfer collateral to this contract
SafeTransferLib.safeTransferFrom(ERC20(C_TOKEN), msg.sender, address(this), _cAmt);

// 2. Borrow debt token
uint256 dAmt = _borrow(_cAmt, _ltv);
// 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);

// 3. Borrow debt token
uint256 dAmt = _borrow(cAmtNet, _ltv);

// 3. Swap debt token for base token
// 4. Swap debt token for base token
(, uint256 bAmt) = _swapExactInput(D_TOKEN, B_TOKEN, dAmt, _swapAmtOutMin, _poolFee);

// 4. Emit event
emit Short(_cAmt, dAmt, bAmt);
// 5. Emit event
emit Short(cAmtNet, dAmt, bAmt);
}

/**
Expand Down
60 changes: 60 additions & 0 deletions src/interfaces/IFeeCollector.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

interface IFeeCollector {
/* solhint-disable func-name-mixedcase */
/* ****************************************************************************
**
** CORE FUNCTIONS
**
******************************************************************************/
/**
* @notice Returns the total balance for the specified token across all client operators.
* @param _token The token address to check.
* @return balance The total balance for the specified token across all client operators.
*/
function totalClientBalances(address _token) external view returns (uint256);

/**
* @notice Returns the balance for the specified token for the specified client operator.
* @param _client A client operator address.
* @param _token The token address to check.
* @return balance The balance for the specified token for the specified client operator.
*/
function balances(address _client, address _token) external view returns (uint256);

/**
* @notice Collects fees from Position contracts when collateral is added.
* @param _client The address, controlled by client operators, for receiving protocol 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.
*/
function collectFees(address _client, address _token, uint256 _amt) external payable;
/**
* @notice Withdraw collected fees from this contract.
* @param _token The token address to withdraw.
*/
function clientWithdraw(address _token) external payable;

/* ****************************************************************************
**
** ADMIN FUNCTIONS
**
******************************************************************************/
/**
* @notice Allows owner to set client rate.
* @param _clientRate The percentage of total transaction-specific protocol fee, allocated to the utilized client.
*/
function setClientRate(uint256 _clientRate) external payable;

/**
* @notice Allows owner to withdraw all of this contract's native token balance.
*/
function extractNative() external payable;

/**
* @notice Allows owner to withdraw protocol fees from this contract.
* @param _token The address of token to remove.
*/
function extractERC20(address _token) external payable;
}
70 changes: 66 additions & 4 deletions src/interfaces/IPosition.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,83 @@ pragma solidity ^0.8.21;

interface IPosition {
/* solhint-disable func-name-mixedcase */
// Meta data
/* ****************************************************************************
**
** METADATA
**
******************************************************************************/
/**
* @notice Returns the owner of this contract.
*/
function OWNER() external returns (address);
/**
* @notice Returns the address of this position's collateral token.
*/
function C_TOKEN() external returns (address);
/**
* @notice Returns the address of this position's debt token.
*/
function D_TOKEN() external returns (address);
/**
* @notice Returns the address of this position's base token (the token that the debt token is swapped for when shorting).
*/
function B_TOKEN() external returns (address);

// Core Functions
function short(uint256 _cAmt, uint256 _ltv, uint256 _swapAmtOutMin, uint24 _poolFee) external payable;
/* ****************************************************************************
**
** CORE FUNCTIONS
**
******************************************************************************/
/**
* @notice Adds to this contract's short position.
* @param _cAmt The amount of collateral to be supplied for this transaction-specific loan (units: C_DECIMALS).
* @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).
*/
function short(uint256 _cAmt, uint256 _ltv, uint256 _swapAmtOutMin, uint24 _poolFee, address _client)
external
payable;

/**
* @notice Fully closes the short position.
* @param _poolFee The fee of the Uniswap pool.
* @param _exactOutput Whether to swap exact output or exact input (true for exact output, false for exact input).
* @param _swapAmtOutMin The minimum amount of output tokens from swap for the tx to go through (only used if _exactOutput is false, supply 0 if true).
* @param _withdrawBuffer The amount of collateral left as safety buffer for tx to go through (default = 100_000, units: 8 decimals).
*/
function close(uint24 _poolFee, bool _exactOutput, uint256 _swapAmtOutMin, uint256 _withdrawBuffer)
external
payable;

/**
* @notice Increases the collateral amount for this contract's loan.
* @param _cAmt The amount of collateral to be supplied (units: C_DECIMALS).
*/
function addCollateral(uint256 _cAmt) external payable;

/**
* @notice Repays any outstanding debt to Aave and transfers remaining collateral from Aave to owner.
* @param _dAmt The amount of debt token to repay to Aave (units: D_DECIMALS).
* To pay off entire debt, _dAmt = debtOwed + smallBuffer (to account for interest).
* @param _withdrawBuffer The amount of collateral left as safety buffer for tx to go through (default = 100_000, units: 8 decimals).
*/
function repayAfterClose(uint256 _dAmt, uint256 _withdrawBuffer) external payable;

// Admin Functions
/* ****************************************************************************
**
** ADMIN FUNCTIONS
**
******************************************************************************/
/**
* @notice Allows owner to withdraw all of this contract's native token balance.
*/
function extractNative() external payable;

/**
* @notice Allows owner to withdraw all of a specified ERC20 token's balance from this contract.
* @param _token The address of token to remove.
*/
function extractERC20(address _token) external payable;
}
Loading

0 comments on commit ed5961b

Please sign in to comment.