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

feat: add and test addLeverage #14

Merged
merged 2 commits into from
Feb 1, 2024
Merged
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
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ The following outlines principles for core protocol funcitonality.

Logic:

- None at the moment🙂
- None for now.

Tests:

- None at the moment🙂
- [ ] Create integration test for multiple calls `add()`
- [ ] Create integration test for multiple calls `addLeverage()`
- [ ] Create integration test for multiple calls `addWithPermit()`
- [ ] Remove all test_ActiveFork

Considerations:

- None at the moment🙂
- None for now.
38 changes: 37 additions & 1 deletion src/Position.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,29 @@ 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 { IERC20Metadata } from "src/interfaces/token/IERC20Metadata.sol";

/// @title Position
/// @author Chain Rule, LLC
/// @notice Manages the owner's individual position
contract Position is DebtService, SwapService {
// Immutables: no SLOAD to save gas
uint8 public immutable B_DECIMALS;
address public immutable B_TOKEN;

// Errors
error TokenConflict();

// Events
event Add(uint256 cAmt, uint256 dAmt, uint256 bAmt);
event AddLeverage(uint256 cAmt, uint256 dAmt, uint256 bAmt);
event Close(uint256 gains);

constructor(address _owner, address _cToken, address _dToken, address _bToken)
DebtService(_owner, _cToken, _dToken)
{
B_TOKEN = _bToken;
B_DECIMALS = IERC20Metadata(_bToken).decimals();
}

/**
Expand Down Expand Up @@ -86,12 +93,41 @@ contract Position is DebtService, SwapService {
add(_cAmt, _ltv, _swapAmtOutMin, _poolFee, _client);
}

/**
* @notice Adds leverage to this contract's short position. This function can only be used for positions where the
* collateral token is the same as the base token.
* @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 of the client operator. Use address(0) if not using a client.
*/
function addLeverage(uint256 _ltv, uint256 _swapAmtOutMin, uint24 _poolFee, address _client)
public
payable
onlyOwner
{
// 1. Ensure that collateral token is the same as the base token
if (C_TOKEN != B_TOKEN) revert TokenConflict();

// 2. Take protocol fee
uint256 bAmtNet = FeeLib.takeProtocolFee(B_TOKEN, IERC20(B_TOKEN).balanceOf(address(this)), _client);

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

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

// 5. Emit event
emit AddLeverage(bAmtNet, dAmt, bAmt);
}

/**
* @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).
* @param _withdrawBuffer The amount of collateral left as safety buffer for tx to go through (at least 100_000 recommended, units: 8 decimals).
*/
function close(uint24 _poolFee, bool _exactOutput, uint256 _swapAmtOutMin, uint256 _withdrawBuffer)
public
Expand Down
31 changes: 28 additions & 3 deletions src/interfaces/IPosition.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ interface IPosition {
*/
function B_TOKEN() external returns (address);

/**
* @notice Returns the number of decimals for this position's collateral token.
*/
function C_DECIMALS() external returns (uint8);

/**
* @notice Returns the number of decimals for this position's debt token.
*/
function D_DECIMALS() external returns (uint8);

/**
* @notice Returns the number of decimals for this position's base token.
*/
function B_DECIMALS() external returns (uint8);

/* ****************************************************************************
**
** CORE FUNCTIONS
Expand Down Expand Up @@ -67,12 +82,22 @@ interface IPosition {
bytes32 _s
) external payable;

/**
* @notice Adds leverage to this contract's short position. This function can only be used for positions where the
* collateral token is the same as the base token.
* @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 of the client operator. Use address(0) if not using a client.
*/
function addLeverage(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).
* @param _withdrawBuffer The amount of collateral left as safety buffer for tx to go through (at least 100_000 recommended, units: 8 decimals).
*/
function close(uint24 _poolFee, bool _exactOutput, uint256 _swapAmtOutMin, uint256 _withdrawBuffer)
external
Expand Down Expand Up @@ -101,7 +126,7 @@ interface IPosition {
* @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).
* @param _withdrawBuffer The amount of collateral left as safety buffer for tx to go through (at least 100_000 recommended, units: 8 decimals).
*/
function repayAfterClose(uint256 _dAmt, uint256 _withdrawBuffer) external payable;

Expand All @@ -110,7 +135,7 @@ interface IPosition {
* with permit, obviating the need for a separate approve tx. This function can only be used for ERC-2612-compliant tokens.
* @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).
* @param _withdrawBuffer The amount of collateral left as safety buffer for tx to go through (at least 100_000 recommended, units: 8 decimals).
* @param _deadline The expiration timestamp of the permit.
* @param _v The V parameter of ERC712 signature for the permit.
* @param _r The R parameter of ERC712 signature for the permit.
Expand Down
10 changes: 5 additions & 5 deletions src/services/DebtService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ contract DebtService is AdminService {
address private constant AAVE_ORACLE = 0xb56c2F0B653B2e0b10C9b928C8580Ac5Df02C7C7;

// Immutables: no SLOAD to save gas
uint8 public immutable C_DECIMALS;
uint8 public immutable D_DECIMALS;
uint64 internal immutable _C_DEC_CONVERSION;
uint64 internal immutable _D_DEC_CONVERSION;
uint8 public immutable C_DECIMALS;
uint8 public immutable D_DECIMALS;
address public immutable C_TOKEN;
address public immutable D_TOKEN;

Expand Down Expand Up @@ -75,7 +75,7 @@ contract DebtService is AdminService {
/**
* @notice Withdraws collateral token from Aave to specified recipient.
* @param _recipient The recipient of the funds.
* @param _buffer The amount of collateral left as safety buffer for tx to go through (default = 100_000, units: 8 decimals).
* @param _buffer The amount of collateral left as safety buffer for tx to go through (at least 100_000 recommended, units: 8 decimals).
*/
function _withdraw(address _recipient, uint256 _buffer) internal {
IPool(AAVE_POOL).withdraw(C_TOKEN, _getMaxWithdrawAmt(_buffer), _recipient);
Expand Down Expand Up @@ -151,7 +151,7 @@ contract DebtService is AdminService {
* @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).
* @param _withdrawBuffer The amount of collateral left as safety buffer for tx to go through (at least 100_000 recommended, units: 8 decimals).
*/
function repayAfterClose(uint256 _dAmt, uint256 _withdrawBuffer) public payable onlyOwner {
SafeTransferLib.safeTransferFrom(ERC20(D_TOKEN), msg.sender, address(this), _dAmt);
Expand All @@ -166,7 +166,7 @@ contract DebtService is AdminService {
* with permit, obviating the need for a separate approve tx. This function can only be used for ERC-2612-compliant tokens.
* @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).
* @param _withdrawBuffer The amount of collateral left as safety buffer for tx to go through (at least 100_000 recommended, units: 8 decimals).
* @param _deadline The expiration timestamp of the permit.
* @param _v The V parameter of ERC712 signature for the permit.
* @param _r The R parameter of ERC712 signature for the permit.
Expand Down
86 changes: 80 additions & 6 deletions test/Position.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import { VmSafe } from "forge-std/Vm.sol";
// Local Imports
import { PositionFactory } from "src/PositionFactory.sol";
import { AdminService } from "src/services/AdminService.sol";
import { Position } from "src/Position.sol";
import {
Assets,
CONTRACT_DEPLOYER,
DAI,
FEE_COLLECTOR,
TEST_CLIENT,
TEST_POOL_FEE,
USDC,
WITHDRAW_BUFFER
} from "test/common/Constants.t.sol";
Expand All @@ -36,6 +38,7 @@ contract PositionTest is Test, TokenUtils, DebtUtils {
PositionFactory public positionFactory;
Assets public assets;
TestPosition[] public positions;
TestPosition[] public diffCTokenBTokenPositions;

// Test Storage
VmSafe.Wallet public wallet;
Expand Down Expand Up @@ -85,6 +88,14 @@ contract PositionTest is Test, TokenUtils, DebtUtils {
}
}
}

// Filter for positions where cToken != bToken and populate diffCTokenBTokenPositions
for (uint256 i = 0; i < positions.length; i++) {
TestPosition memory currentPosition = positions[i];
if (currentPosition.cToken != currentPosition.bToken) {
diffCTokenBTokenPositions.push(currentPosition);
}
}
}

/// @dev
Expand All @@ -95,7 +106,7 @@ contract PositionTest is Test, TokenUtils, DebtUtils {

/// @dev
// - It should revert with Unauthorized() error when called by an unauthorized sender.
function testFuzz_CannotShort(address _sender) public {
function testFuzz_CannotAdd(address _sender) public {
// Setup
uint256 ltv = 60;

Expand All @@ -120,7 +131,66 @@ contract PositionTest is Test, TokenUtils, DebtUtils {
// Act
vm.prank(_sender);
vm.expectRevert(AdminService.Unauthorized.selector);
IPosition(addr).add(cAmt, ltv, 0, 3000, TEST_CLIENT);
IPosition(addr).add(cAmt, ltv, 0, TEST_POOL_FEE, TEST_CLIENT);

// Revert to snapshot
vm.revertTo(id);
}
}

/// @dev
// - It should revert with TokenConflict() error when cToken != bToken.
function test_CannotAddLeverageTokenConflict() public {
// Setup
uint256 ltv = 60;

// Take snapshot
uint256 id = vm.snapshot();

for (uint256 i; i < diffCTokenBTokenPositions.length; i++) {
// Test variables
address addr = diffCTokenBTokenPositions[i].addr;
address bToken = diffCTokenBTokenPositions[i].bToken;
uint256 bAmt = assets.maxCAmts(bToken);

// Fund contract with bToken
_fund(addr, bToken, bAmt);

// Act
vm.prank(owner);
vm.expectRevert(Position.TokenConflict.selector);
IPosition(addr).addLeverage(ltv, 0, TEST_POOL_FEE, TEST_CLIENT);

// Revert to snapshot
vm.revertTo(id);
}
}

/// @dev
// - It should revert with Unauthorized() error when called by an unauthorized sender.
function testFuzz_CannotAddLeverageUnauthorized(address _sender) public {
// Setup
uint256 ltv = 60;

// Take snapshot
uint256 id = vm.snapshot();

for (uint256 i; i < diffCTokenBTokenPositions.length; i++) {
// Test variables
address addr = diffCTokenBTokenPositions[i].addr;
address bToken = diffCTokenBTokenPositions[i].bToken;
uint256 bAmt = assets.maxCAmts(bToken);

// Assumptions
vm.assume(_sender != owner);

// Fund contract with bToken
_fund(addr, bToken, bAmt);

// Act
vm.prank(_sender);
vm.expectRevert(AdminService.Unauthorized.selector);
IPosition(addr).addLeverage(ltv, 0, TEST_POOL_FEE, TEST_CLIENT);

// Revert to snapshot
vm.revertTo(id);
Expand All @@ -129,15 +199,17 @@ contract PositionTest is Test, TokenUtils, DebtUtils {

/// @dev
// - It should revert with Unauthorized() error when called by an unauthorized sender.
function testFuzz_CannotShortWithPermit(address _sender) public {
function testFuzz_CannotAddWithPermit(address _sender) public {
// Setup
uint256 ltv = 60;

// Take snapshot
uint256 id = vm.snapshot();

for (uint256 i; i < positions.length; i++) {
// Test variables
address cToken = positions[i].cToken;
uint256 cAmt = assets.maxCAmts(cToken);
uint256 ltv = 60;

// Assumptions
vm.assume(_sender != owner);
Expand All @@ -152,7 +224,9 @@ contract PositionTest is Test, TokenUtils, DebtUtils {
// Act
vm.prank(_sender);
vm.expectRevert(AdminService.Unauthorized.selector);
IPosition(positions[i].addr).addWithPermit(cAmt, ltv, 0, 3000, TEST_CLIENT, permitTimestamp, v, r, s);
IPosition(positions[i].addr).addWithPermit(
cAmt, ltv, 0, TEST_POOL_FEE, TEST_CLIENT, permitTimestamp, v, r, s
);

// Revert to snapshot
vm.revertTo(id);
Expand Down Expand Up @@ -180,7 +254,7 @@ contract PositionTest is Test, TokenUtils, DebtUtils {
_fund(owner, positions[i].cToken, cAmt);
vm.startPrank(owner);
IERC20(positions[i].cToken).approve(addr, cAmt);
IPosition(addr).add(cAmt, ltv, 0, 3000, TEST_CLIENT);
IPosition(addr).add(cAmt, ltv, 0, TEST_POOL_FEE, TEST_CLIENT);
vm.stopPrank();

// Act
Expand Down
2 changes: 2 additions & 0 deletions test/PositionFactory.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,15 @@ contract PositionFactoryTest is Test, TokenUtils {
cToken = IPosition(position).C_TOKEN();
dToken = IPosition(position).D_TOKEN();
bToken = IPosition(position).B_TOKEN();
uint8 bDecimals = assets.decimals(IPosition(position).B_TOKEN());

// Assertions
assertNotEq(position, address(0));
assertEq(owner, positionOwner);
assertEq(cToken, supportedAssets[i]);
assertEq(dToken, supportedAssets[j]);
assertEq(bToken, supportedAssets[k]);
assertEq(IPosition(position).B_DECIMALS(), bDecimals);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions test/common/Constants.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ address constant WBTC = 0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f;
address constant USDC_HOLDER = 0x47c031236e19d024b42f8AE6780E44A573170703;

// Uint constants
uint24 constant TEST_POOL_FEE = 3000;
uint256 constant PROFIT_PERCENT = 25;
uint256 constant REPAY_PERCENT = 75;
uint256 constant WITHDRAW_BUFFER = 100_000;
Expand Down
Loading
Loading