Skip to content

Commit

Permalink
feat: add and test addLeverage (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xernesto authored Feb 1, 2024
1 parent 807f3ab commit d76bcc8
Show file tree
Hide file tree
Showing 14 changed files with 620 additions and 50 deletions.
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

0 comments on commit d76bcc8

Please sign in to comment.