Skip to content
Open
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
2 changes: 1 addition & 1 deletion brownie-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ networks:
default: mainnet-fork

# automatically fetch contract sources from Etherscan
autofetch_sources: False
autofetch_sources: True

# require OpenZepplin Contracts
dependencies:
Expand Down
39 changes: 30 additions & 9 deletions contracts/Joint.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ interface ProviderStrategy {
function want() external view returns (address);

function totalDebt() external view returns (uint256);

function borrowedToken() external view returns (address);

function updatedBalanceOfDebt() external returns (uint256);

function balanceOfDebt() external view returns (uint256);
}

abstract contract Joint {
Expand Down Expand Up @@ -124,17 +130,18 @@ abstract contract Joint {
address _reward
) internal virtual {
require(address(providerA) == address(0), "Joint already initialized");
require(_providerA == _providerB, "!lev-providers");// should be the same for this version
providerA = ProviderStrategy(_providerA);
providerB = ProviderStrategy(_providerB);
providerB = ProviderStrategy(_providerB);
router = _router;
WETH = _weth;
reward = _reward;

// NOTE: we let some loss to avoid getting locked in the position if something goes slightly wrong
maxPercentageLoss = 500; // 0.1%
maxPercentageLoss = 500; // 0.5%

tokenA = address(providerA.want());
tokenB = address(providerB.want());
tokenB = address(providerB.borrowedToken());
require(tokenA != tokenB, "!same-want");
pair = IUniswapV2Pair(getPair());

Expand Down Expand Up @@ -227,12 +234,29 @@ abstract contract Joint {
);
}

// Poorman's rebalance
// if we ended up with more tokenB than required, we sell it for tokenA
// if we ended up with less tokenB than required, we buy it with tokenA
// TODO: improve gas efficiency by merging with previous block !!
currentBalanceB = IERC20(tokenB).balanceOf(address(this));
uint256 requiredBalanceB = providerA.updatedBalanceOfDebt();
if(requiredBalanceB > currentBalanceB) {
uint256[] memory inAmounts =
IUniswapV2Router02(router).getAmountsIn(
requiredBalanceB.sub(currentBalanceB),
getTokenOutPath(tokenA, tokenB)
);
sellCapital(tokenA, tokenB, inAmounts[0]);
} else if (currentBalanceB > requiredBalanceB){
sellCapital(tokenB, tokenA, currentBalanceB.sub(requiredBalanceB));
}

// reset invested balances
investedA = investedB = 0;

_returnLooseToProviders();

// Check that we have returned with no losses
//
require(
IERC20(tokenA).balanceOf(address(providerA)) >=
providerA
Expand All @@ -242,11 +266,8 @@ abstract contract Joint {
"!wrong-balanceA"
);
require(
IERC20(tokenB).balanceOf(address(providerB)) >=
providerB
.totalDebt()
.mul(RATIO_PRECISION.sub(maxPercentageLoss))
.div(RATIO_PRECISION),
IERC20(tokenB).balanceOf(address(providerA)) >=
providerA.balanceOfDebt(),
"!wrong-balanceB"
);
}
Expand Down
134 changes: 132 additions & 2 deletions contracts/ProviderStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ import {
BaseStrategyInitializable
} from "@yearnvaults/contracts/BaseStrategy.sol";

import "../interfaces/ironbank/CErc20Interface.sol";
import "../interfaces/ironbank/ComptrollerInterface.sol";

interface IPriceOracle {
function getUnderlyingPrice(CErc20Interface ibToken)
external
view
returns (uint256);
}

interface JointAPI {
function closePositionReturnFunds() external;

Expand All @@ -40,16 +50,32 @@ interface JointAPI {
function dontInvestWant() external view returns (bool);
}

contract ProviderStrategy is BaseStrategyInitializable {
interface IPriceProvider {
function latestAnswer() external view returns (uint);
}

contract LevProviderStrategy is BaseStrategyInitializable {
using SafeERC20 for IERC20;
using Address for address;
using SafeMath for uint256;

CErc20Interface public ibToken;
ComptrollerInterface public comptrollerIB;

IPriceProvider public priceProvider = IPriceProvider(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419);

address public joint;

bool public forceLiquidate;

constructor(address _vault) public BaseStrategyInitializable(_vault) {}
uint256 internal constant BLOCKS_PER_YEAR = 2_102_400;

constructor(address _vault, CErc20Interface _ibToken, ComptrollerInterface _comptrollerIB) public BaseStrategyInitializable(_vault) {
comptrollerIB = _comptrollerIB;
ibToken = _ibToken;
IERC20 _borrowedToken = IERC20(_ibToken.underlying());
_borrowedToken.safeApprove(address(_ibToken), type(uint256).max);
}

function name() external view override returns (string memory) {
return
Expand Down Expand Up @@ -87,6 +113,11 @@ contract ProviderStrategy is BaseStrategyInitializable {
// The Provider will always ask the joint to close the position before harvesting
JointAPI(joint).closePositionReturnFunds();

// Repay debt
if(balanceOfDebt() > 0) {
repayFullDebt();
}

// After closePosition, the provider will always have funds in its own balance (not in joint)
uint256 _totalDebt = totalDebt();
uint256 totalAssets = balanceOfWant();
Expand Down Expand Up @@ -144,12 +175,20 @@ contract ProviderStrategy is BaseStrategyInitializable {
return;
}

// Take debt: this function will borrow an equivalent amount to the amount of want
borrowRequiredAmountTokenB();
uint256 bTokenBalance = balanceOfBorrowedToken();
if(bTokenBalance > 0) {
IERC20(borrowedToken()).transfer(joint, bTokenBalance);
}

// Using a push approach (instead of pull)
uint256 wantBalance = balanceOfWant();
if (wantBalance > 0) {
want.transfer(joint, wantBalance);
}
JointAPI(joint).openPosition();

}

function liquidatePosition(uint256 _amountNeeded)
Expand All @@ -167,6 +206,7 @@ contract ProviderStrategy is BaseStrategyInitializable {
}

function prepareMigration(address _newStrategy) internal override {
// TODO: return debt! (handle levered balance before migrating)
JointAPI(joint).migrateProvider(_newStrategy);
}

Expand All @@ -181,6 +221,8 @@ contract ProviderStrategy is BaseStrategyInitializable {
return IERC20(want).balanceOf(address(this));
}



function setJoint(address _joint) external onlyGovernance {
require(
JointAPI(_joint).providerA() == address(this) ||
Expand All @@ -205,6 +247,7 @@ contract ProviderStrategy is BaseStrategyInitializable {
{
uint256 expectedBalance = estimatedTotalAssets();
JointAPI(joint).closePositionReturnFunds();

_amountFreed = balanceOfWant();
// NOTE: we accept a 1% difference before reverting
require(
Expand Down Expand Up @@ -261,4 +304,91 @@ contract ProviderStrategy is BaseStrategyInitializable {
_path[2] = _token_out;
}
}

function borrowedToken() public view returns(address) {
return ibToken.underlying();
}

function balanceOfBorrowedToken() public view returns (uint) {
return IERC20(borrowedToken()).balanceOf(address(this));
}

function updatedBalanceOfDebt() public returns (uint256) {
return ibToken.borrowBalanceCurrent(address(this));
}

function balanceOfDebt() public view returns (uint256) {
return ibToken.borrowBalanceStored(address(this));
}

function repayFullDebt() internal {
repayBorrow(balanceOfDebt());
}


function borrowRequiredAmountTokenB() internal {
// TODO: make this generic
uint256 amountBToBorrow = balanceOfWant().mul(priceProvider.latestAnswer()).mul(uint(10)**IERC20Extended(borrowedToken()).decimals()).div(1e26);
borrow(amountBToBorrow);
}

function borrow(uint256 amount) internal returns (uint256) {
uint256 currentBorrow = updatedBalanceOfDebt();
uint256 creditLimit =
getCreditLimitInBorrowedToken(address(this));
uint256 availableLimit = creditLimit > currentBorrow ? creditLimit - currentBorrow : 0;
uint256 maxBorrow = Math.min(ibToken.getCash(), availableLimit);
uint256 borrowAmount = Math.min(amount, maxBorrow);
require(ibToken.borrow(borrowAmount) == 0);
return borrowAmount;
}

function repayBorrow(uint256 amount) internal returns (uint256) {
uint256 maxRepay = Math.min(balanceOfBorrowedToken(), balanceOfDebt());
uint256 repayAmount = Math.min(amount, maxRepay);
require(ibToken.repayBorrow(repayAmount) == 0);
return repayAmount;
}

function currentBorrowingCosts() public view returns (uint256) {
return ironBankBorrowRateAfterChange(0, false);
}

function ironBankBorrowRateAfterChange(uint256 amount, bool repay)
public
view
returns (uint256 annualBorrowingCost)
{
uint256 borrowRatePerBlock =
ibToken.estimateBorrowRatePerBlockAfterChange(amount, repay);

// calculate estimated annual costs
annualBorrowingCost = borrowRatePerBlock * BLOCKS_PER_YEAR;
}

function getCreditLimitInBorrowedToken(address account)
public
view
returns (uint256)
{
// returns USD value in mantissa (1e18)
uint256 usdCreditLimit = comptrollerIB.creditLimits(account);
// if credit limit is infinite, we don't need to check anything else (otherwise, we will get an overflow)
if (usdCreditLimit == type(uint256).max) {
return type(uint256).max;
}
uint256 priceUSD = getBorrowedTokenPriceUSD();
// we need to adjust price AND decimals
// Using simplified version of:
// uint256 wantCreditLimit = usdCreditLimit * 1e18 * (10 ** want.decimals()) / priceUSD / 1e18;
uint256 wantCreditLimit =
(usdCreditLimit * (uint256(10) ** IERC20Extended(borrowedToken()).decimals())) / priceUSD;
return wantCreditLimit;
}

function getBorrowedTokenPriceUSD() public view returns (uint256) {
return IPriceOracle(comptrollerIB.oracle()).getUnderlyingPrice(ibToken);
}


}
Loading