diff --git a/.env.template b/.env.template index 93de3e0..de2542a 100644 --- a/.env.template +++ b/.env.template @@ -85,6 +85,18 @@ REPORT_GAS=false # CURVE_3POOL_MINTR=0xd061D61a4d941c39E5453435B6345Dc261C2fcE0 # CURVE_CRV=0xD533a949740bb3306d119CC777fa900bA034cd52 +# StrategyBarnBridgeUSDC +# BARN_BRIDGE_USDC_SMART_YIELD=0x4B8d90D68F26DEF303Dcb6CFc9b63A1aAEC15840 +# BARN_BRIDGE_USDC_COMP_PROVIDER_POOL=0xDAA037F99d168b552c0c61B7Fb64cF7819D78310 +# BARN_BRIDGE_USDC_YIELD_FARM=0x68Af34129755091E22F91899cEAC48657e5a5062 +# BARN_BRIDGE_BOND=0x0391D2021f89DC339F60Fff84546EA23E337750f + +# StrategyBarnBridgeDAI +# BARN_BRIDGE_DAI_SMART_YIELD=0x673f9488619821aB4f4155FdFFe06f6139De518F +# BARN_BRIDGE_DAI_COMP_PROVIDER_POOL=0xe6c1A8E7a879d7feBB8144276a62f9a6b381bd37 +# Currently BarnBride Yield Farm does not support DAI +# BARN_BRIDGE_DAI_YIELD_FARM= + # IMPERSONATED_DEPLOYER=0xab5801a7d398351b8be11c439e05c5b3259aec9b # ETH_FUNDER=0xab5801a7d398351b8be11c439e05c5b3259aec9b # DAI_FUNDER=0xf977814e90da44bfa03b6295a0616a897441acec @@ -95,6 +107,7 @@ REPORT_GAS=false # WETH_FUNDER=0x0f4ee9631f4be0a63756515141281a3e2b293bbe # COMPOUND_COMP_FUNDER=0xbe0eb53f46cd790cd13851d5eff43d12404d33e8 # CURVE_CRV_FUNDER=0xf977814e90da44bfa03b6295a0616a897441acec +# BARN_BRIDGE_BOND_FUNDER=0x0F4ee9631f4be0a63756515141281A3E2B293Bbe # DUMMY_ASSET= # DUMMY_FUNDER= @@ -107,6 +120,7 @@ REPORT_GAS=false # STRATEGY_CURVE_3POOL_DAI= # STRATEGY_CURVE_3POOL_USDC= # STRATEGY_CURVE_3POOL_USDT= +# STRATEGY_BARN_BRIDGE_JC_USDC= # ROLLUP_CHAIN= # REGISTRY= diff --git a/contracts/strategies/StrategyBarnBridgeJToken.sol b/contracts/strategies/StrategyBarnBridgeJToken.sol new file mode 100644 index 0000000..ce2c4fd --- /dev/null +++ b/contracts/strategies/StrategyBarnBridgeJToken.sol @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0 <0.8.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +import "./interfaces/BarnBridge/IISmartYield.sol"; +import "./interfaces/BarnBridge/IYieldFarmContinuous.sol"; +import "./dependencies/BarnBridge/SmartYield.sol"; +import "./dependencies/BarnBridge/YieldFarm/YieldFarmContinuous.sol"; + +import "./interfaces/IStrategy.sol"; +import "./interfaces/uniswap/IUniswapV2.sol"; + +/** + * @notice Deposits ERC20 token into Barn Bridge Smart Yield and issues stBarnBridgeJToken(e.g. stBarnBridgeJcUSDC) in L2. + */ +contract StrategyBarnBridgeJToken is IStrategy, Ownable { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + // The address of BarnBridge smart yield + address public smartYield; + SmartYield smartYieldContract; + // The address of compound provider pool + address public compProviderPool; + + // Info of supplying erc20 token to Smart Yield + // The symbol of the supplying token + string public symbol; + // The address of supplying token(e.g. USDC, DAI) + address public supplyToken; + + // The address of junior token(e.g. bb_cUSDC, bb_cDAI) + // This address is the same as the smartYield address + address public jToken; + + // The address of BarnBridge Yield Farom Continuous + address public yieldFarm; + YieldFarmContinuous yieldFarmContract; + // The address of BarnBridge governance token + address public bond; + + address public uniswap; + + address public controller; + + constructor( + address _smartYield, + address _compProviderPool, + string memory _symbol, + address _yieldFarm, + address _supplyToken, + address _bond, + address _uniswap, + address _controller + ) { + smartYield = _smartYield; + smartYieldContract = SmartYield(_smartYield); + compProviderPool = _compProviderPool; + symbol = _symbol; + supplyToken = _supplyToken; + jToken = _smartYield; + yieldFarm = _yieldFarm; + yieldFarmContract = YieldFarmContinuous(_yieldFarm); + bond = _bond; + uniswap = _uniswap; + controller = _controller; + } + + /** + * @dev Require that the caller must be an EOA account to avoid flash loans. + */ + modifier onlyEOA() { + require(msg.sender == tx.origin, "Not EOA"); + _; + } + + function getAssetAddress() external view override returns (address) { + return supplyToken; + } + + function syncBalance() external override returns (uint256) { + uint256 jTokenBalance = yieldFarmContract.balances(address(this)); + // Calculate share of jTokenBalance in the debt + uint256 forfeits = calForfeits(jTokenBalance); + // jTokenPrice is jToken price * 1e18 + uint256 jTokenPrice = IISmartYield(smartYield).price(); + return jTokenBalance.mul(jTokenPrice).div(1e18).sub(forfeits); + } + + function calForfeits(uint256 jTokenAmount) public view returns (uint256) { + // share of jTokenAmount in the debt + uint256 debtShare = jTokenAmount.mul(1e18).div(IERC20(jToken).totalSupply()); + uint256 forfeits = IISmartYield(smartYield).abondDebt().mul(debtShare).div(1e18); + return forfeits; + } + + function harvest() external override onlyEOA { + IYieldFarmContinuous(yieldFarm).claim(); + uint256 bondBalance = IERC20(bond).balanceOf(address(this)); + if (bondBalance > 0) { + // Sell BOND for more supplying token(e.g. USDC, DAI) + IERC20(bond).safeIncreaseAllowance(uniswap, bondBalance); + + address[] memory paths = new address[](2); + paths[0] = bond; + paths[1] = supplyToken; + + IUniswapV2(uniswap).swapExactTokensForTokens( + bondBalance, + uint256(0), + paths, + address(this), + block.timestamp.add(1800) + ); + + uint256 obtainedSupplyTokenAmount = IERC20(supplyToken).balanceOf(address(this)); + IERC20(supplyToken).safeIncreaseAllowance(compProviderPool, obtainedSupplyTokenAmount); + IISmartYield(smartYield).buyTokens( + obtainedSupplyTokenAmount, + uint256(0), + block.timestamp.add(1800) + ); + + // Stake junior token(e.g. bb_cUSDC, bb_cDAI) to Yield Farm for earn BOND token + uint256 jTokenBalance = IERC20(jToken).balanceOf(address(this)); + IERC20(jToken).safeIncreaseAllowance(yieldFarm, jTokenBalance); + IYieldFarmContinuous(yieldFarm).deposit(jTokenBalance); + } + } + + function aggregateCommit(uint256 _commitAmount) external override { + require(msg.sender == controller, "Not controller"); + require(_commitAmount > 0, "Nothing to commit"); + + // Pull supplying token(e.g. USDC, DAI) from Controller + IERC20(supplyToken).safeTransferFrom(msg.sender, address(this), _commitAmount); + + // Buy junior token(e.g. bb_cUSDC, bb_cDAI) + IERC20(supplyToken).safeIncreaseAllowance(compProviderPool, _commitAmount); + IISmartYield(smartYield).buyTokens( + _commitAmount, + uint256(0), + block.timestamp.add(1800) + ); + + // Stake junior token to Yield Farm for earn BOND token + uint256 jTokenBalance = IERC20(jToken).balanceOf(address(this)); + IERC20(jToken).safeIncreaseAllowance(yieldFarm, jTokenBalance); + IYieldFarmContinuous(yieldFarm).deposit(jTokenBalance); + + emit Committed(_commitAmount); + } + + function aggregateUncommit(uint256 _uncommitAmount) external override { + require(msg.sender == controller, "Not controller"); + require(_uncommitAmount > 0, "Nothing to uncommit"); + + // Unstake junior token(e.g. bb_cUSDC, bb_cDAI) from Yield Farm + // jTokenPrice is junior token price * 1e18 + uint256 jTokenPrice = ISmartYield(smartYield).price(); + uint256 jTokenWithdrawAmount = _uncommitAmount.mul(1e18).div(jTokenPrice); + IYieldFarmContinuous(yieldFarm).withdraw(jTokenWithdrawAmount); + + // Instant withdraw + IISmartYield(smartYield).sellTokens( + jTokenWithdrawAmount, + uint256(0), + block.timestamp.add(1800) + ); + + // Transfer supply token to Controller + uint256 supplyTokenBalance = IERC20(supplyToken).balanceOf(address(this)); + IERC20(supplyToken).safeTransfer(controller, supplyTokenBalance); + + emit UnCommitted(_uncommitAmount); + } + + function setController(address _controller) external onlyOwner { + emit ControllerChanged(controller, _controller); + controller = _controller; + } +} diff --git a/contracts/strategies/dependencies/BarnBridge/Governed.sol b/contracts/strategies/dependencies/BarnBridge/Governed.sol new file mode 100644 index 0000000..0075058 --- /dev/null +++ b/contracts/strategies/dependencies/BarnBridge/Governed.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.6.0 <0.8.0; +pragma abicoder v2; + +abstract contract Governed { + + address public dao; + address public guardian; + + modifier onlyDao { + require( + dao == msg.sender, + "GOV: not dao" + ); + _; + } + + modifier onlyDaoOrGuardian { + require( + msg.sender == dao || msg.sender == guardian, + "GOV: not dao/guardian" + ); + _; + } + + constructor() + { + dao = msg.sender; + guardian = msg.sender; + } + + function setDao(address dao_) + external + onlyDao + { + dao = dao_; + } + + function setGuardian(address guardian_) + external + onlyDao + { + guardian = guardian_; + } + +} \ No newline at end of file diff --git a/contracts/strategies/dependencies/BarnBridge/IBond.sol b/contracts/strategies/dependencies/BarnBridge/IBond.sol new file mode 100644 index 0000000..f82e71a --- /dev/null +++ b/contracts/strategies/dependencies/BarnBridge/IBond.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.6.0 <0.8.0; +pragma abicoder v2; + +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +interface IBond is IERC721 { + function smartYield() external view returns (address); + + function mint(address to, uint256 tokenId) external; + + function burn(uint256 tokenId) external; +} \ No newline at end of file diff --git a/contracts/strategies/dependencies/BarnBridge/IController.sol b/contracts/strategies/dependencies/BarnBridge/IController.sol new file mode 100644 index 0000000..16d1c10 --- /dev/null +++ b/contracts/strategies/dependencies/BarnBridge/IController.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.6.0 <0.8.0; +pragma abicoder v2; + +import "./Governed.sol"; +import "./IProvider.sol"; +import "./ISmartYield.sol"; + +abstract contract IController is Governed { + + uint256 public constant EXP_SCALE = 1e18; + + address public pool; // compound provider pool + + address public smartYield; // smartYield + + address public oracle; // IYieldOracle + + address public bondModel; // IBondModel + + address public feesOwner; // fees are sent here + + // max accepted cost of harvest when converting COMP -> underlying, + // if harvest gets less than (COMP to underlying at spot price) - HARVEST_COST%, it will revert. + // if it gets more, the difference goes to the harvest caller + uint256 public HARVEST_COST = 40 * 1e15; // 4% + + // fee for buying jTokens + uint256 public FEE_BUY_JUNIOR_TOKEN = 3 * 1e15; // 0.3% + + // fee for redeeming a sBond + uint256 public FEE_REDEEM_SENIOR_BOND = 100 * 1e15; // 10% + + // max rate per day for sBonds + uint256 public BOND_MAX_RATE_PER_DAY = 719065000000000; // APY 30% / year + + // max duration of a purchased sBond + uint16 public BOND_LIFE_MAX = 90; // in days + + bool public PAUSED_BUY_JUNIOR_TOKEN = false; + + bool public PAUSED_BUY_SENIOR_BOND = false; + + function setHarvestCost(uint256 newValue_) + public + onlyDao + { + require( + HARVEST_COST < EXP_SCALE, + "IController: HARVEST_COST too large" + ); + HARVEST_COST = newValue_; + } + + function setBondMaxRatePerDay(uint256 newVal_) + public + onlyDao + { + BOND_MAX_RATE_PER_DAY = newVal_; + } + + function setBondLifeMax(uint16 newVal_) + public + onlyDao + { + BOND_LIFE_MAX = newVal_; + } + + function setFeeBuyJuniorToken(uint256 newVal_) + public + onlyDao + { + FEE_BUY_JUNIOR_TOKEN = newVal_; + } + + function setFeeRedeemSeniorBond(uint256 newVal_) + public + onlyDao + { + FEE_REDEEM_SENIOR_BOND = newVal_; + } + + function setPaused(bool buyJToken_, bool buySBond_) + public + onlyDaoOrGuardian + { + PAUSED_BUY_JUNIOR_TOKEN = buyJToken_; + PAUSED_BUY_SENIOR_BOND = buySBond_; + } + + function setOracle(address newVal_) + public + onlyDao + { + oracle = newVal_; + } + + function setBondModel(address newVal_) + public + onlyDao + { + bondModel = newVal_; + } + + function setFeesOwner(address newVal_) + public + onlyDao + { + feesOwner = newVal_; + } + + function yieldControllTo(address newController_) + public + onlyDao + { + IProvider(pool).setController(newController_); + ISmartYield(smartYield).setController(newController_); + } + + function providerRatePerDay() external virtual returns (uint256); +} diff --git a/contracts/strategies/dependencies/BarnBridge/IProvider.sol b/contracts/strategies/dependencies/BarnBridge/IProvider.sol new file mode 100644 index 0000000..0ec25ae --- /dev/null +++ b/contracts/strategies/dependencies/BarnBridge/IProvider.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.6.0 <0.8.0; +pragma abicoder v2; + +interface IProvider { + + function smartYield() external view returns (address); + + function controller() external view returns (address); + + function underlyingFees() external view returns (uint256); + + // deposit underlyingAmount_ into provider, add takeFees_ to fees + function _depositProvider(uint256 underlyingAmount_, uint256 takeFees_) external; + + // withdraw underlyingAmount_ from provider, add takeFees_ to fees + function _withdrawProvider(uint256 underlyingAmount_, uint256 takeFees_) external; + + function _takeUnderlying(address from_, uint256 amount_) external; + + function _sendUnderlying(address to_, uint256 amount_) external; + + function transferFees() external; + + // current total underlying balance as measured by the provider pool, without fees + function underlyingBalance() external returns (uint256); + + function setController(address newController_) external; +} diff --git a/contracts/strategies/dependencies/BarnBridge/ISmartYield.sol b/contracts/strategies/dependencies/BarnBridge/ISmartYield.sol new file mode 100644 index 0000000..f93be1f --- /dev/null +++ b/contracts/strategies/dependencies/BarnBridge/ISmartYield.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.6.0 <0.8.0; +pragma abicoder v2; + +interface ISmartYield { + + // a senior BOND (metadata for NFT) + struct SeniorBond { + // amount seniors put in + uint256 principal; + // amount yielded at the end. total = principal + gain + uint256 gain; + // bond was issued at timestamp + uint256 issuedAt; + // bond matures at timestamp + uint256 maturesAt; + // was it liquidated yet + bool liquidated; + } + + // a junior BOND (metadata for NFT) + struct JuniorBond { + // amount of tokens (jTokens) junior put in + uint256 tokens; + // bond matures at timestamp + uint256 maturesAt; + } + + // a checkpoint for all JuniorBonds with same maturity date JuniorBond.maturesAt + struct JuniorBondsAt { + // sum of JuniorBond.tokens for JuniorBonds with the same JuniorBond.maturesAt + uint256 tokens; + // price at which JuniorBonds will be paid. Initially 0 -> unliquidated (price is in the future or not yet liquidated) + uint256 price; + } + + function controller() external view returns (address); + + function buyBond(uint256 principalAmount_, uint256 minGain_, uint256 deadline_, uint16 forDays_) external returns (uint256); + + function redeemBond(uint256 bondId_) external; + + function unaccountBonds(uint256[] memory bondIds_) external; + + function buyTokens(uint256 underlyingAmount_, uint256 minTokens_, uint256 deadline_) external; + + /** + * sell all tokens instantly + */ + function sellTokens(uint256 tokens_, uint256 minUnderlying_, uint256 deadline_) external; + + function buyJuniorBond(uint256 tokenAmount_, uint256 maxMaturesAt_, uint256 deadline_) external; + + function redeemJuniorBond(uint256 jBondId_) external; + + function liquidateJuniorBonds(uint256 upUntilTimestamp_) external; + + /** + * token purchase price + */ + function price() external returns (uint256); + + function abondPaid() external view returns (uint256); + + function abondDebt() external view returns (uint256); + + function abondGain() external view returns (uint256); + + /** + * @notice current total underlying balance, without accruing interest + */ + function underlyingTotal() external returns (uint256); + + /** + * @notice current underlying loanable, without accruing interest + */ + function underlyingLoanable() external returns (uint256); + + function underlyingJuniors() external returns (uint256); + + function bondGain(uint256 principalAmount_, uint16 forDays_) external returns (uint256); + + function maxBondDailyRate() external returns (uint256); + + function setController(address newController_) external; +} \ No newline at end of file diff --git a/contracts/strategies/dependencies/BarnBridge/JuniorToken.sol b/contracts/strategies/dependencies/BarnBridge/JuniorToken.sol new file mode 100644 index 0000000..61ac714 --- /dev/null +++ b/contracts/strategies/dependencies/BarnBridge/JuniorToken.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.6.0 <0.8.0; +pragma abicoder v2; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +abstract contract JuniorToken is ERC20 { + + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_ + ) + ERC20(name_, symbol_) + { + _setupDecimals(decimals_); + } + +} \ No newline at end of file diff --git a/contracts/strategies/dependencies/BarnBridge/SmartYield.sol b/contracts/strategies/dependencies/BarnBridge/SmartYield.sol new file mode 100644 index 0000000..7551c3a --- /dev/null +++ b/contracts/strategies/dependencies/BarnBridge/SmartYield.sol @@ -0,0 +1,722 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.6.0 <0.8.0; +pragma abicoder v2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "./lib/math/MathUtils.sol"; + +import "./IController.sol"; +import "./ISmartYield.sol"; + +import "./IProvider.sol"; + +import "./model/IBondModel.sol"; +import "./IBond.sol"; +import "./JuniorToken.sol"; + +contract SmartYield is + JuniorToken, + ISmartYield +{ + using SafeMath for uint256; + + uint256 public constant MAX_UINT256 = uint256(-1); + uint256 public constant EXP_SCALE = 1e18; + + // controller address + address public override controller; + + // address of IProviderPool + address public pool; + + // senior BOND (NFT) + address public seniorBond; // IBond + + // junior BOND (NFT) + address public juniorBond; // IBond + + // underlying amount in matured and liquidated juniorBonds + uint256 public underlyingLiquidatedJuniors; + + // tokens amount in unmatured juniorBonds or matured and unliquidated + uint256 public tokensInJuniorBonds; + + // latest SeniorBond Id + uint256 public seniorBondId; + + // latest JuniorBond Id + uint256 public juniorBondId; + + // last index of juniorBondsMaturities that was liquidated + uint256 public juniorBondsMaturitiesPrev; + // list of junior bond maturities (timestamps) + uint256[] public juniorBondsMaturities; + + // checkpoints for all JuniorBonds matureing at (timestamp) -> (JuniorBondsAt) + // timestamp -> JuniorBondsAt + mapping(uint256 => JuniorBondsAt) public juniorBondsMaturingAt; + + // metadata for senior bonds + // bond id => bond (SeniorBond) + mapping(uint256 => SeniorBond) public seniorBonds; + + // metadata for junior bonds + // bond id => bond (JuniorBond) + mapping(uint256 => JuniorBond) public juniorBonds; + + // pool state / average bond + // holds rate of payment by juniors to seniors + SeniorBond public abond; + + bool public _setup; + + // emitted when user buys junior ERC20 tokens + event BuyTokens(address indexed buyer, uint256 underlyingIn, uint256 tokensOut, uint256 fee); + // emitted when user sells junior ERC20 tokens and forfeits their share of the debt + event SellTokens(address indexed seller, uint256 tokensIn, uint256 underlyingOut, uint256 forfeits); + + event BuySeniorBond(address indexed buyer, uint256 indexed seniorBondId, uint256 underlyingIn, uint256 gain, uint256 forDays); + + event RedeemSeniorBond(address indexed owner, uint256 indexed seniorBondId, uint256 fee); + + event BuyJuniorBond(address indexed buyer, uint256 indexed juniorBondId, uint256 tokensIn, uint256 maturesAt); + + event RedeemJuniorBond(address indexed owner, uint256 indexed juniorBondId, uint256 underlyingOut); + + modifier onlyControllerOrDao { + require( + msg.sender == controller || msg.sender == IController(controller).dao(), + "PPC: only controller/DAO" + ); + _; + } + + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_ + ) + JuniorToken(name_, symbol_, decimals_) + {} + + function setup( + address controller_, + address pool_, + address seniorBond_, + address juniorBond_ + ) + external + { + require( + false == _setup, + "SY: already setup" + ); + + controller = controller_; + pool = pool_; + seniorBond = seniorBond_; + juniorBond = juniorBond_; + + _setup = true; + } + + // externals + + // change the controller, only callable by old controller or dao + function setController(address newController_) + external override + onlyControllerOrDao + { + controller = newController_; + } + + // buy at least _minTokens with _underlyingAmount, before _deadline passes + function buyTokens( + uint256 underlyingAmount_, + uint256 minTokens_, + uint256 deadline_ + ) + external override + { + _beforeProviderOp(block.timestamp); + + require( + false == IController(controller).PAUSED_BUY_JUNIOR_TOKEN(), + "SY: buyTokens paused" + ); + + require( + block.timestamp <= deadline_, + "SY: buyTokens deadline" + ); + + uint256 fee = MathUtils.fractionOf(underlyingAmount_, IController(controller).FEE_BUY_JUNIOR_TOKEN()); + // (underlyingAmount_ - fee) * EXP_SCALE / price() + uint256 getsTokens = (underlyingAmount_.sub(fee)).mul(EXP_SCALE).div(price()); + + require( + getsTokens >= minTokens_, + "SY: buyTokens minTokens" + ); + + // --- + + address buyer = msg.sender; + + IProvider(pool)._takeUnderlying(buyer, underlyingAmount_); + IProvider(pool)._depositProvider(underlyingAmount_, fee); + _mint(buyer, getsTokens); + + emit BuyTokens(buyer, underlyingAmount_, getsTokens, fee); + } + + // sell _tokens for at least _minUnderlying, before _deadline and forfeit potential future gains + function sellTokens( + uint256 tokenAmount_, + uint256 minUnderlying_, + uint256 deadline_ + ) + external override + { + _beforeProviderOp(block.timestamp); + + require( + block.timestamp <= deadline_, + "SY: sellTokens deadline" + ); + + // share of these tokens in the debt + // tokenAmount_ * EXP_SCALE / totalSupply() + uint256 debtShare = tokenAmount_.mul(EXP_SCALE).div(totalSupply()); + // (abondDebt() * debtShare) / EXP_SCALE + uint256 forfeits = abondDebt().mul(debtShare).div(EXP_SCALE); + // debt share is forfeit, and only diff is returned to user + // (tokenAmount_ * price()) / EXP_SCALE - forfeits + uint256 toPay = tokenAmount_.mul(price()).div(EXP_SCALE).sub(forfeits); + + require( + toPay >= minUnderlying_, + "SY: sellTokens minUnderlying" + ); + + // --- + + address seller = msg.sender; + + _burn(seller, tokenAmount_); + IProvider(pool)._withdrawProvider(toPay, 0); + IProvider(pool)._sendUnderlying(seller, toPay); + + emit SellTokens(seller, tokenAmount_, toPay, forfeits); + } + + // Purchase a senior bond with principalAmount_ underlying for forDays_, buyer gets a bond with gain >= minGain_ or revert. deadline_ is timestamp before which tx is not rejected. + // returns gain + function buyBond( + uint256 principalAmount_, + uint256 minGain_, + uint256 deadline_, + uint16 forDays_ + ) + external override + returns (uint256) + { + _beforeProviderOp(block.timestamp); + + require( + false == IController(controller).PAUSED_BUY_SENIOR_BOND(), + "SY: buyBond paused" + ); + + require( + block.timestamp <= deadline_, + "SY: buyBond deadline" + ); + + require( + 0 < forDays_ && forDays_ <= IController(controller).BOND_LIFE_MAX(), + "SY: buyBond forDays" + ); + + uint256 gain = bondGain(principalAmount_, forDays_); + + require( + gain >= minGain_, + "SY: buyBond minGain" + ); + + require( + gain > 0, + "SY: buyBond gain 0" + ); + + require( + gain < underlyingLoanable(), + "SY: buyBond underlyingLoanable" + ); + + uint256 issuedAt = block.timestamp; + + // --- + + address buyer = msg.sender; + + IProvider(pool)._takeUnderlying(buyer, principalAmount_); + IProvider(pool)._depositProvider(principalAmount_, 0); + + SeniorBond memory b = + SeniorBond( + principalAmount_, + gain, + issuedAt, + uint256(1 days) * uint256(forDays_) + issuedAt, + false + ); + + _mintBond(buyer, b); + + emit BuySeniorBond(buyer, seniorBondId, principalAmount_, gain, forDays_); + + return gain; + } + + // buy an nft with tokenAmount_ jTokens, that matures at abond maturesAt + function buyJuniorBond( + uint256 tokenAmount_, + uint256 maxMaturesAt_, + uint256 deadline_ + ) + external override + { + _beforeProviderOp(block.timestamp); + + // 1 + abond.maturesAt / EXP_SCALE + uint256 maturesAt = abond.maturesAt.div(EXP_SCALE).add(1); + + require( + block.timestamp <= deadline_, + "SY: buyJuniorBond deadline" + ); + + require( + maturesAt <= maxMaturesAt_, + "SY: buyJuniorBond maxMaturesAt" + ); + + JuniorBond memory jb = JuniorBond( + tokenAmount_, + maturesAt + ); + + // --- + + address buyer = msg.sender; + + _takeTokens(buyer, tokenAmount_); + _mintJuniorBond(buyer, jb); + + emit BuyJuniorBond(buyer, juniorBondId, tokenAmount_, maturesAt); + + // if abond.maturesAt is past we can liquidate, but juniorBondsMaturingAt might have already been liquidated + if (block.timestamp >= maturesAt) { + JuniorBondsAt memory jBondsAt = juniorBondsMaturingAt[jb.maturesAt]; + + if (jBondsAt.price == 0) { + _liquidateJuniorsAt(jb.maturesAt); + } else { + // juniorBondsMaturingAt was previously liquidated, + _burn(address(this), jb.tokens); // burns user's locked tokens reducing the jToken supply + // underlyingLiquidatedJuniors += jb.tokens * jBondsAt.price / EXP_SCALE + underlyingLiquidatedJuniors = underlyingLiquidatedJuniors.add( + jb.tokens.mul(jBondsAt.price).div(EXP_SCALE) + ); + _unaccountJuniorBond(jb); + } + return this.redeemJuniorBond(juniorBondId); + } + } + + // Redeem a senior bond by it's id. Anyone can redeem but owner gets principal + gain + function redeemBond( + uint256 bondId_ + ) + external override + { + _beforeProviderOp(block.timestamp); + + require( + block.timestamp >= seniorBonds[bondId_].maturesAt, + "SY: redeemBond not matured" + ); + + // bondToken.ownerOf will revert for burned tokens + address payTo = IBond(seniorBond).ownerOf(bondId_); + // seniorBonds[bondId_].gain + seniorBonds[bondId_].principal + uint256 payAmnt = seniorBonds[bondId_].gain.add(seniorBonds[bondId_].principal); + uint256 fee = MathUtils.fractionOf(seniorBonds[bondId_].gain, IController(controller).FEE_REDEEM_SENIOR_BOND()); + payAmnt = payAmnt.sub(fee); + + // --- + + if (seniorBonds[bondId_].liquidated == false) { + seniorBonds[bondId_].liquidated = true; + _unaccountBond(seniorBonds[bondId_]); + } + + // bondToken.burn will revert for already burned tokens + IBond(seniorBond).burn(bondId_); + + IProvider(pool)._withdrawProvider(payAmnt, fee); + IProvider(pool)._sendUnderlying(payTo, payAmnt); + + emit RedeemSeniorBond(payTo, bondId_, fee); + } + + // once matured, redeem a jBond for underlying + function redeemJuniorBond(uint256 jBondId_) + external override + { + _beforeProviderOp(block.timestamp); + + JuniorBond memory jb = juniorBonds[jBondId_]; + require( + jb.maturesAt <= block.timestamp, + "SY: redeemJuniorBond maturesAt" + ); + + JuniorBondsAt memory jBondsAt = juniorBondsMaturingAt[jb.maturesAt]; + + // blows up if already burned + address payTo = IBond(juniorBond).ownerOf(jBondId_); + // jBondsAt.price * jb.tokens / EXP_SCALE + uint256 payAmnt = jBondsAt.price.mul(jb.tokens).div(EXP_SCALE); + + // --- + + _burnJuniorBond(jBondId_); + IProvider(pool)._withdrawProvider(payAmnt, 0); + IProvider(pool)._sendUnderlying(payTo, payAmnt); + underlyingLiquidatedJuniors = underlyingLiquidatedJuniors.sub(payAmnt); + + emit RedeemJuniorBond(payTo, jBondId_, payAmnt); + } + + // returns the maximum theoretically possible daily rate for senior bonds, + // in reality the actual rate given to a bond will always be lower due to slippage + function maxBondDailyRate() + external override + returns (uint256) + { + return IBondModel(IController(controller).bondModel()).maxDailyRate( + underlyingTotal(), + underlyingLoanable(), + IController(controller).providerRatePerDay() + ); + } + + function liquidateJuniorBonds(uint256 upUntilTimestamp_) + external override + { + require( + upUntilTimestamp_ <= block.timestamp, + "SY: liquidateJuniorBonds in future" + ); + _beforeProviderOp(upUntilTimestamp_); + } + + // /externals + + // publics + + // given a principal amount and a number of days, compute the guaranteed bond gain, excluding principal + function bondGain(uint256 principalAmount_, uint16 forDays_) + public override + returns (uint256) + { + return IBondModel(IController(controller).bondModel()).gain( + underlyingTotal(), + underlyingLoanable(), + IController(controller).providerRatePerDay(), + principalAmount_, + forDays_ + ); + } + + // jToken price * EXP_SCALE + function price() + public override + returns (uint256) + { + uint256 ts = totalSupply(); + // (ts == 0) ? EXP_SCALE : (underlyingJuniors() * EXP_SCALE) / ts + return (ts == 0) ? EXP_SCALE : underlyingJuniors().mul(EXP_SCALE).div(ts); + } + + function underlyingTotal() + public virtual override + returns(uint256) + { + // underlyingBalance() - underlyingLiquidatedJuniors + return IProvider(pool).underlyingBalance().sub(underlyingLiquidatedJuniors); + } + + function underlyingJuniors() + public virtual override + returns (uint256) + { + // underlyingTotal() - abond.principal - abondPaid() + return underlyingTotal().sub(abond.principal).sub(abondPaid()); + } + + function underlyingLoanable() + public virtual override + returns (uint256) + { + // underlyingTotal - abond.principal - abond.gain - queued withdrawls + uint256 _underlyingTotal = underlyingTotal(); + // abond.principal - abond.gain - (tokensInJuniorBonds * price() / EXP_SCALE) + uint256 _lockedUnderlying = abond.principal.add(abond.gain).add( + tokensInJuniorBonds.mul(price()).div(EXP_SCALE) + ); + + if (_lockedUnderlying > _underlyingTotal) { + // abond.gain and (tokensInJuniorBonds in underlying) can overlap, so there is a cases where _lockedUnderlying > _underlyingTotal + return 0; + } + + // underlyingTotal() - abond.principal - abond.gain - (tokensInJuniorBonds * price() / EXP_SCALE) + return _underlyingTotal.sub(_lockedUnderlying); + } + + function abondGain() + public view override + returns (uint256) + { + return abond.gain; + } + + function abondPaid() + public view override + returns (uint256) + { + uint256 ts = block.timestamp * EXP_SCALE; + if (ts <= abond.issuedAt || (abond.maturesAt <= abond.issuedAt)) { + return 0; + } + + uint256 duration = abond.maturesAt.sub(abond.issuedAt); + uint256 paidDuration = MathUtils.min(ts.sub(abond.issuedAt), duration); + // abondGain() * paidDuration / duration + return abondGain().mul(paidDuration).div(duration); + } + + function abondDebt() + public view override + returns (uint256) + { + // abondGain() - abondPaid() + return abondGain().sub(abondPaid()); + } + + // /publics + + // internals + + // liquidates junior bonds up to upUntilTimestamp_ timestamp + function _beforeProviderOp(uint256 upUntilTimestamp_) internal { + // this modifier will be added to the begginging of all (write) functions. + // The first tx after a queued liquidation's timestamp will trigger the liquidation + // reducing the jToken supply, and setting aside owed_dai for withdrawals + for (uint256 i = juniorBondsMaturitiesPrev; i < juniorBondsMaturities.length; i++) { + if (upUntilTimestamp_ >= juniorBondsMaturities[i]) { + _liquidateJuniorsAt(juniorBondsMaturities[i]); + juniorBondsMaturitiesPrev = i.add(1); + } else { + break; + } + } + } + + function _liquidateJuniorsAt(uint256 timestamp_) + internal + { + JuniorBondsAt storage jBondsAt = juniorBondsMaturingAt[timestamp_]; + + require( + jBondsAt.tokens > 0, + "SY: nothing to liquidate" + ); + + require( + jBondsAt.price == 0, + "SY: already liquidated" + ); + + jBondsAt.price = price(); + + // --- + + // underlyingLiquidatedJuniors += jBondsAt.tokens * jBondsAt.price / EXP_SCALE; + underlyingLiquidatedJuniors = underlyingLiquidatedJuniors.add( + jBondsAt.tokens.mul(jBondsAt.price).div(EXP_SCALE) + ); + _burn(address(this), jBondsAt.tokens); // burns Junior locked tokens reducing the jToken supply + tokensInJuniorBonds = tokensInJuniorBonds.sub(jBondsAt.tokens); + } + + // removes matured seniorBonds from being accounted in abond + function unaccountBonds(uint256[] memory bondIds_) + external override + { + uint256 currentTime = block.timestamp; + + for (uint256 f = 0; f < bondIds_.length; f++) { + if ( + currentTime >= seniorBonds[bondIds_[f]].maturesAt && + seniorBonds[bondIds_[f]].liquidated == false + ) { + seniorBonds[bondIds_[f]].liquidated = true; + _unaccountBond(seniorBonds[bondIds_[f]]); + } + } + } + + function _mintBond(address to_, SeniorBond memory bond_) + internal + { + require( + seniorBondId < MAX_UINT256, + "SY: _mintBond" + ); + + seniorBondId++; + seniorBonds[seniorBondId] = bond_; + _accountBond(bond_); + IBond(seniorBond).mint(to_, seniorBondId); + } + + // when a new bond is added to the pool, we want: + // - to average abond.maturesAt (the earliest date at which juniors can fully exit), this shortens the junior exit date compared to the date of the last active bond + // - to keep the price for jTokens before a bond is bought ~equal with the price for jTokens after a bond is bought + function _accountBond(SeniorBond memory b_) + internal + { + uint256 _now = block.timestamp * EXP_SCALE; + + //abondDebt() + b_.gain + uint256 newDebt = abondDebt().add(b_.gain); + // for the very first bond or the first bond after abond maturity: abondDebt() = 0 => newMaturesAt = b.maturesAt + // (abond.maturesAt * abondDebt() + b_.maturesAt * EXP_SCALE * b_.gain) / newDebt + uint256 newMaturesAt = (abond.maturesAt.mul(abondDebt()).add(b_.maturesAt.mul(EXP_SCALE).mul(b_.gain))).div(newDebt); + + // (uint256(1) + ((abond.gain + b_.gain) * (newMaturesAt - _now)) / newDebt) + uint256 newDuration = (abond.gain.add(b_.gain)).mul(newMaturesAt.sub(_now)).div(newDebt).add(1); + // timestamp = timestamp - tokens * d / tokens + uint256 newIssuedAt = newMaturesAt.sub(newDuration, "SY: liquidate some seniorBonds"); + + abond = SeniorBond( + abond.principal.add(b_.principal), + abond.gain.add(b_.gain), + newIssuedAt, + newMaturesAt, + false + ); + } + + // when a bond is redeemed from the pool, we want: + // - for abond.maturesAt (the earliest date at which juniors can fully exit) to remain the same as before the redeem + // - to keep the price for jTokens before a bond is bought ~equal with the price for jTokens after a bond is bought + function _unaccountBond(SeniorBond memory b_) + internal + { + uint256 now_ = block.timestamp * EXP_SCALE; + + if ((now_ >= abond.maturesAt)) { + // abond matured + // abondDebt() == 0 + abond = SeniorBond( + abond.principal.sub(b_.principal), + abond.gain - b_.gain, + now_.sub(abond.maturesAt.sub(abond.issuedAt)), + now_, + false + ); + + return; + } + // uint256(1) + (abond.gain - b_.gain) * (abond.maturesAt - now_) / abondDebt() + uint256 newDuration = (abond.gain.sub(b_.gain)).mul(abond.maturesAt.sub(now_)).div(abondDebt()).add(1); + // timestamp = timestamp - tokens * d / tokens + uint256 newIssuedAt = abond.maturesAt.sub(newDuration, "SY: liquidate some seniorBonds"); + + abond = SeniorBond( + abond.principal.sub(b_.principal), + abond.gain.sub(b_.gain), + newIssuedAt, + abond.maturesAt, + false + ); + } + + function _mintJuniorBond(address to_, JuniorBond memory jb_) + internal + { + require( + juniorBondId < MAX_UINT256, + "SY: _mintJuniorBond" + ); + + juniorBondId++; + juniorBonds[juniorBondId] = jb_; + + _accountJuniorBond(jb_); + IBond(juniorBond).mint(to_, juniorBondId); + } + + function _accountJuniorBond(JuniorBond memory jb_) + internal + { + // tokensInJuniorBonds += jb_.tokens + tokensInJuniorBonds = tokensInJuniorBonds.add(jb_.tokens); + + JuniorBondsAt storage jBondsAt = juniorBondsMaturingAt[jb_.maturesAt]; + uint256 tmp; + + if (jBondsAt.tokens == 0 && block.timestamp < jb_.maturesAt) { + juniorBondsMaturities.push(jb_.maturesAt); + for (uint256 i = juniorBondsMaturities.length - 1; i >= MathUtils.max(1, juniorBondsMaturitiesPrev); i--) { + if (juniorBondsMaturities[i] > juniorBondsMaturities[i - 1]) { + break; + } + tmp = juniorBondsMaturities[i - 1]; + juniorBondsMaturities[i - 1] = juniorBondsMaturities[i]; + juniorBondsMaturities[i] = tmp; + } + } + + // jBondsAt.tokens += jb_.tokens + jBondsAt.tokens = jBondsAt.tokens.add(jb_.tokens); + } + + function _burnJuniorBond(uint256 bondId_) internal { + // blows up if already burned + IBond(juniorBond).burn(bondId_); + } + + function _unaccountJuniorBond(JuniorBond memory jb_) internal { + // tokensInJuniorBonds -= jb_.tokens; + tokensInJuniorBonds = tokensInJuniorBonds.sub(jb_.tokens); + JuniorBondsAt storage jBondsAt = juniorBondsMaturingAt[jb_.maturesAt]; + // jBondsAt.tokens -= jb_.tokens; + jBondsAt.tokens = jBondsAt.tokens.sub(jb_.tokens); + } + + function _takeTokens(address from_, uint256 amount_) internal { + _transfer(from_, address(this), amount_); + } + + // /internals + +} \ No newline at end of file diff --git a/contracts/strategies/dependencies/BarnBridge/YieldFarm/YieldFarmContinuous.sol b/contracts/strategies/dependencies/BarnBridge/YieldFarm/YieldFarmContinuous.sol new file mode 100644 index 0000000..0a239e2 --- /dev/null +++ b/contracts/strategies/dependencies/BarnBridge/YieldFarm/YieldFarmContinuous.sol @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.6.0 <0.8.0; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "../ISmartYield.sol"; +import "./YieldFarmGoverned.sol"; + +contract YieldFarmContinuous is YieldFarmGoverned { + using SafeMath for uint256; + using SafeERC20 for IERC20; + + uint256 constant multiplierScale = 10 ** 18; + + IERC20 public poolToken; + IERC20 public rewardToken; + + uint256 public rewardNotTransferred; + uint256 public balanceBefore; + uint256 public currentMultiplier; + + mapping(address => uint256) public balances; + mapping(address => uint256) public userMultiplier; + mapping(address => uint256) public owed; + + uint256 public poolSize; + + event Claim(address indexed user, uint256 amount); + event Deposit(address indexed user, uint256 amount, uint256 balanceAfter); + event Withdraw(address indexed user, uint256 amount, uint256 balanceAfter); + + constructor(address _owner, address _rewardToken, address _poolToken) { + require(_rewardToken != address(0), "reward token must not be 0x0"); + require(_poolToken != address(0), "pool token must not be 0x0"); + + transferOwnership(_owner); + + rewardToken = IERC20(_rewardToken); + poolToken = IERC20(_poolToken); + } + + function deposit(uint256 amount) public { + require(amount > 0, "amount must be greater than 0"); + + require( + poolToken.allowance(msg.sender, address(this)) >= amount, + "allowance must be greater than 0" + ); + + // it is important to calculate the amount owed to the user before doing any changes + // to the user's balance or the pool's size + _calculateOwed(msg.sender); + + uint256 newBalance = balances[msg.sender].add(amount); + balances[msg.sender] = newBalance; + poolSize = poolSize.add(amount); + + poolToken.safeTransferFrom(msg.sender, address(this), amount); + + emit Deposit(msg.sender, amount, newBalance); + } + + function withdraw(uint256 amount) public { + require(amount > 0, "amount must be greater than 0"); + + uint256 currentBalance = balances[msg.sender]; + require(currentBalance >= amount, "insufficient balance"); + + // it is important to calculate the amount owed to the user before doing any changes + // to the user's balance or the pool's size + _calculateOwed(msg.sender); + + uint256 newBalance = currentBalance.sub(amount); + balances[msg.sender] = newBalance; + poolSize = poolSize.sub(amount); + + poolToken.safeTransfer(msg.sender, amount); + + emit Withdraw(msg.sender, amount, newBalance); + } + + // claim calculates the currently owed reward and transfers the funds to the user + function claim() public returns (uint256){ + _calculateOwed(msg.sender); + + uint256 amount = owed[msg.sender]; + if (amount == 0) { + return 0; + } + + // check if there's enough balance to distribute the amount owed to the user + // otherwise, pull the rewardNotTransferred from source + if (rewardToken.balanceOf(address(this)) < amount) { + pullRewardFromSource(); + } + + owed[msg.sender] = 0; + + rewardToken.safeTransfer(msg.sender, amount); + + // acknowledge the amount that was transferred to the user + balanceBefore = balanceBefore.sub(amount); + + emit Claim(msg.sender, amount); + + return amount; + } + + function withdrawAndClaim(uint256 amount) public returns (uint256) { + withdraw(amount); + return claim(); + } + + // ackFunds checks the difference between the last known balance of `token` and the current one + // if it goes up, the multiplier is re-calculated + // if it goes down, it only updates the known balance + function ackFunds() public { + uint256 balanceNow = rewardToken.balanceOf(address(this)).add(rewardNotTransferred); + uint256 balanceBeforeLocal = balanceBefore; + + if (balanceNow <= balanceBeforeLocal || balanceNow == 0) { + balanceBefore = balanceNow; + return; + } + + // if there's no bond staked, it doesn't make sense to ackFunds because there's nobody to distribute them to + // and the calculation would fail anyways due to division by 0 + uint256 poolSizeLocal = poolSize; + if (poolSizeLocal == 0) { + return; + } + + uint256 diff = balanceNow.sub(balanceBeforeLocal); + uint256 multiplier = currentMultiplier.add(diff.mul(multiplierScale).div(poolSizeLocal)); + + balanceBefore = balanceNow; + currentMultiplier = multiplier; + } + + // pullRewardFromSource transfers any amount due from the source to this contract so it can be distributed + function pullRewardFromSource() public override { + softPullReward(); + + uint256 amountToTransfer = rewardNotTransferred; + + // if there's nothing to transfer, stop the execution + if (amountToTransfer == 0) { + return; + } + + rewardNotTransferred = 0; + + rewardToken.safeTransferFrom(rewardSource, address(this), amountToTransfer); + } + + // softPullReward calculates the reward accumulated since the last time it was called but does not actually + // execute the transfers. Instead, it adds the amount to rewardNotTransferred variable + function softPullReward() public { + uint256 lastPullTs = lastSoftPullTs; + + // no need to execute multiple times in the same block + if (lastPullTs == block.timestamp) { + return; + } + + uint256 rate = rewardRatePerSecond; + address source = rewardSource; + + // don't execute if the setup was not completed + if (rate == 0 || source == address(0)) { + return; + } + + // if there's no allowance left on the source contract, don't try to pull anything else + uint256 allowance = rewardToken.allowance(source, address(this)); + if (allowance == 0 || allowance <= rewardNotTransferred) { + return; + } + + uint256 timeSinceLastPull = block.timestamp.sub(lastPullTs); + uint256 amountToPull = timeSinceLastPull.mul(rate); + + // only pull the minimum between allowance left and the amount that should be pulled for the period + uint256 allowanceLeft = allowance.sub(rewardNotTransferred); + if (amountToPull > allowanceLeft) { + amountToPull = allowanceLeft; + } + + rewardNotTransferred = rewardNotTransferred.add(amountToPull); + lastSoftPullTs = block.timestamp; + } + + // rewardLeft returns the amount that was not yet distributed + // even though it is not a view, this function is only intended for external use + function rewardLeft() external returns (uint256) { + softPullReward(); + + return rewardToken.allowance(rewardSource, address(this)).sub(rewardNotTransferred); + } + + // _calculateOwed calculates and updates the total amount that is owed to an user and updates the user's multiplier + // to the current value + // it automatically attempts to pull the token from the source and acknowledge the funds + function _calculateOwed(address user) internal { + softPullReward(); + ackFunds(); + + uint256 reward = _userPendingReward(user); + + owed[user] = owed[user].add(reward); + userMultiplier[user] = currentMultiplier; + } + + // _userPendingReward calculates the reward that should be based on the current multiplier / anything that's not included in the `owed[user]` value + // it does not represent the entire reward that's due to the user unless added on top of `owed[user]` + function _userPendingReward(address user) internal view returns (uint256) { + uint256 multiplier = currentMultiplier.sub(userMultiplier[user]); + + return balances[user].mul(multiplier).div(multiplierScale); + } +} diff --git a/contracts/strategies/dependencies/BarnBridge/YieldFarm/YieldFarmGoverned.sol b/contracts/strategies/dependencies/BarnBridge/YieldFarm/YieldFarmGoverned.sol new file mode 100644 index 0000000..f25a8e0 --- /dev/null +++ b/contracts/strategies/dependencies/BarnBridge/YieldFarm/YieldFarmGoverned.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.6.0 <0.8.0; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/access/Ownable.sol"; + +abstract contract YieldFarmGoverned is Ownable { + address public rewardSource; + uint256 public rewardRatePerSecond; + + uint256 public lastSoftPullTs; + + function setRewardsSource(address src) public { + require(msg.sender == owner(), "only owner can call"); + require(src != address(0), "source cannot be 0x0"); + + rewardSource = src; + } + + function setRewardRatePerSecond(uint256 rate) public { + require(msg.sender == owner(), "only owner can call"); + + // pull everything due until now to not be affected by the change in rate + pullRewardFromSource(); + + rewardRatePerSecond = rate; + + // it's the first time the rate is set which is equivalent to starting the rewards + if (lastSoftPullTs == 0) { + lastSoftPullTs = block.timestamp; + } + } + + function pullRewardFromSource() public virtual; +} \ No newline at end of file diff --git a/contracts/strategies/dependencies/BarnBridge/lib/math/MathUtils.sol b/contracts/strategies/dependencies/BarnBridge/lib/math/MathUtils.sol new file mode 100644 index 0000000..6ddd0cd --- /dev/null +++ b/contracts/strategies/dependencies/BarnBridge/lib/math/MathUtils.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.6.0 <0.8.0; + +import "@openzeppelin/contracts/math/SafeMath.sol"; + +library MathUtils { + + using SafeMath for uint256; + + uint256 public constant EXP_SCALE = 1e18; + + function min(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = x < y ? x : y; + } + + function max(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = x > y ? x : y; + } + + function compound( + // in wei + uint256 principal, + // rate is * EXP_SCALE + uint256 ratePerPeriod, + uint16 periods + ) internal pure returns (uint256) { + if (0 == ratePerPeriod) { + return principal; + } + + while (periods > 0) { + // principal += principal * ratePerPeriod / EXP_SCALE; + principal = principal.add(principal.mul(ratePerPeriod).div(EXP_SCALE)); + periods -= 1; + } + + return principal; + } + + function compound2( + uint256 principal, + uint256 ratePerPeriod, + uint16 periods + ) internal pure returns (uint256) { + if (0 == ratePerPeriod) { + return principal; + } + + while (periods > 0) { + if (periods % 2 == 1) { + //principal += principal * ratePerPeriod / EXP_SCALE; + principal = principal.add(principal.mul(ratePerPeriod).div(EXP_SCALE)); + periods -= 1; + } else { + //ratePerPeriod = ((2 * ratePerPeriod * EXP_SCALE) + (ratePerPeriod * ratePerPeriod)) / EXP_SCALE; + ratePerPeriod = ((uint256(2).mul(ratePerPeriod).mul(EXP_SCALE)).add(ratePerPeriod.mul(ratePerPeriod))).div(EXP_SCALE); + periods /= 2; + } + } + + return principal; + } + + // computes a * f / EXP_SCALE + function fractionOf(uint256 a, uint256 f) internal pure returns (uint256) { + return a.mul(f).div(EXP_SCALE); + } + +} diff --git a/contracts/strategies/dependencies/BarnBridge/model/IBondModel.sol b/contracts/strategies/dependencies/BarnBridge/model/IBondModel.sol new file mode 100644 index 0000000..10480be --- /dev/null +++ b/contracts/strategies/dependencies/BarnBridge/model/IBondModel.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.6.0 <0.8.0; +pragma abicoder v2; + +interface IBondModel { + + function gain(uint256 total_, uint256 loanable_, uint256 dailyRate_, uint256 principal_, uint16 forDays_) external pure returns (uint256); + + function maxDailyRate(uint256 total_, uint256 loanable_, uint256 dailyRate_) external pure returns (uint256); + +} \ No newline at end of file diff --git a/contracts/strategies/interfaces/BarnBridge/IISmartYield.sol b/contracts/strategies/interfaces/BarnBridge/IISmartYield.sol new file mode 100644 index 0000000..cd8dd67 --- /dev/null +++ b/contracts/strategies/interfaces/BarnBridge/IISmartYield.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0 <0.8.0; + +interface IISmartYield { + /** + * buy at least _minTokens with _underlyingAmount, before _deadline passes + */ + function buyTokens(uint256 underlyingAmount_, uint256 minTokens_, uint256 deadline_) external; + + /** + * sell _tokens for at least _minUnderlying, before _deadline and forfeit potential future gains + */ + function sellTokens(uint256 tokenAmount_, uint256 minUnderlying_, uint256 deadline_) external; + + /** + * jToken price * 1e18 + */ + function price() external returns (uint256); + + function abondDebt() external view returns (uint256); +} \ No newline at end of file diff --git a/contracts/strategies/interfaces/BarnBridge/IYieldFarmContinuous.sol b/contracts/strategies/interfaces/BarnBridge/IYieldFarmContinuous.sol new file mode 100644 index 0000000..d6cb04a --- /dev/null +++ b/contracts/strategies/interfaces/BarnBridge/IYieldFarmContinuous.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0 <0.8.0; + +interface IYieldFarmContinuous { + function deposit(uint256 amount) external; + function withdraw(uint256 amount) external; + function claim() external returns (uint256); +} \ No newline at end of file diff --git a/deploy/strategies/004_barnbridge_usdc.ts b/deploy/strategies/004_barnbridge_usdc.ts new file mode 100644 index 0000000..80a9a85 --- /dev/null +++ b/deploy/strategies/004_barnbridge_usdc.ts @@ -0,0 +1,32 @@ +import * as dotenv from 'dotenv'; +import { DeployFunction } from 'hardhat-deploy/types'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; + +dotenv.config(); + +const strategyContractName = "StrategyBarnBridgeJcUSDC"; +const strategyDeploymentName = "StrategyBarnBridgeUSDC"; + +const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { deployments, getNamedAccounts } = hre; + const { deploy } = deployments; + const { deployer } = await getNamedAccounts(); + + await deploy(strategyContractName, { + from: deployer, + log: true, + args: [ + process.env.SMART_YIELD, + process.env.COMP_PROVIDER_POOL, + process.env.YIELD_FARM, + process.env.USDC, + process.env.JCUSDC, + process.env.BOND, + process.env.UNISWAP_ROUTER, + process.env.ROLLUP_CHAIN + ] + }); +}; + +deployFunc.tags = [strategyDeploymentName]; +export default deployFunc; \ No newline at end of file diff --git a/deploy/strategies/013_barnbridge_usdc.ts b/deploy/strategies/013_barnbridge_usdc.ts new file mode 100644 index 0000000..6dc5b79 --- /dev/null +++ b/deploy/strategies/013_barnbridge_usdc.ts @@ -0,0 +1,32 @@ +import * as dotenv from 'dotenv'; +import { DeployFunction } from 'hardhat-deploy/types'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; + +dotenv.config(); + +const strategyContractName = "StrategyBarnBridgeJToken"; +const strategyDeploymentName = "StrategyBarnBridgeJcUSDC"; + +const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { deployments, getNamedAccounts } = hre; + const { deploy } = deployments; + const { deployer } = await getNamedAccounts(); + + await deploy(strategyContractName, { + from: deployer, + log: true, + args: [ + process.env.BARN_BRIDGE_USDC_SMART_YIELD, + process.env.BARN_BRIDGE_USDC_COMP_PROVIDER_POOL, + 'USDC', + process.env.BARN_BRIDGE_USDC_YIELD_FARM, + process.env.USDC, + process.env.BARN_BRIDGE_BOND, + process.env.UNISWAP_ROUTER, + process.env.ROLLUP_CHAIN + ] + }); +}; + +deployFunc.tags = [strategyDeploymentName]; +export default deployFunc; \ No newline at end of file diff --git a/test-strategy/StrategyBarnBridgeJcUSDC.spec.ts b/test-strategy/StrategyBarnBridgeJcUSDC.spec.ts new file mode 100644 index 0000000..ad7bffa --- /dev/null +++ b/test-strategy/StrategyBarnBridgeJcUSDC.spec.ts @@ -0,0 +1,158 @@ +import { expect } from 'chai'; +import * as dotenv from 'dotenv'; +import { ethers, network } from 'hardhat'; + +import { getAddress } from '@ethersproject/address'; +import { MaxUint256 } from '@ethersproject/constants'; +import { formatEther, parseEther, parseUnits, formatUnits } from '@ethersproject/units'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'; + +import { ERC20__factory } from '../typechain/factories/ERC20__factory'; +import { StrategyBarnBridgeJToken__factory } from '../typechain/factories/StrategyBarnBridgeJToken__factory'; +import { StrategyBarnBridgeJToken } from '../typechain/StrategyBarnBridgeJToken'; +import { ensureBalanceAndApproval, getDeployerSigner } from './common'; + +import { SmartYield__factory } from '../typechain/factories/SmartYield__factory'; +import { SmartYield } from '../typechain/SmartYield'; + +dotenv.config(); + +describe('StrategyBarnBridgeJcUSDC', function () { + async function deploy() { + const deployerSigner = await getDeployerSigner(); + + let strategy: StrategyBarnBridgeJToken; + const deployedAddress = process.env.STRATEGY_BARN_BRIDGE_JC_USDC; + if (deployedAddress) { + strategy = StrategyBarnBridgeJToken__factory.connect(deployedAddress, deployerSigner); + } else { + const StrategyBarnBridgeJTokenFactory = (await ethers.getContractFactory( + 'StrategyBarnBridgeJToken' + )) as StrategyBarnBridgeJToken__factory; + strategy = await StrategyBarnBridgeJTokenFactory + .connect(deployerSigner) + .deploy( + process.env.BARN_BRIDGE_USDC_SMART_YIELD as string, + process.env.BARN_BRIDGE_USDC_COMP_PROVIDER_POOL as string, + 'USDC', + process.env.BARN_BRIDGE_USDC_YIELD_FARM as string, + process.env.USDC as string, + process.env.BARN_BRIDGE_BOND as string, + process.env.UNISWAP_ROUTER as string, + deployerSigner.address + ); + await strategy.deployed(); + } + + const usdc = ERC20__factory.connect(process.env.USDC as string, deployerSigner); + + return { strategy, usdc, deployerSigner }; + } + + it('should commit, uncommit and optionally harvest', async function () { + this.timeout(300000); + + const { strategy, usdc, deployerSigner } = await deploy(); + + expect(getAddress(await strategy.getAssetAddress())).to.equal(getAddress(usdc.address)); + + const strategyBalanceBeforeCommit = await strategy.callStatic.syncBalance(); + console.log('Strategy USDC balance before commit:', formatUnits(strategyBalanceBeforeCommit, 6)); + const controllerBalanceBeforeCommit = await usdc.balanceOf(deployerSigner.address); + console.log('Controller USDC balance before commit:', formatUnits(controllerBalanceBeforeCommit, 6)); + + const smartYield = SmartYield__factory.connect(process.env.BARN_BRIDGE_USDC_SMART_YIELD as string, deployerSigner); + const beforCommitJTokenPrice = await smartYield.callStatic.price(); + console.log('bb_cUSDC price before commit:', formatEther(beforCommitJTokenPrice)); + + const commitAmount = parseUnits('1', 6); + await ensureBalanceAndApproval( + usdc, + 'USDC', + commitAmount, + deployerSigner, + strategy.address, + process.env.USDC_FUNDER as string + ) + + console.log('===== Commit 1 USDC ====='); + // Currently buy junior token fee is 0.5% + const fee = parseUnits('0.005', 6); + + const commitGas = await strategy.estimateGas.aggregateCommit(commitAmount); + expect(commitGas.lte(800000)).to.be.true; + const commitTx = await strategy.aggregateCommit(commitAmount, { gasLimit: 800000 }); + await commitTx.wait(); + + const afterCommitJTokenPrice = await smartYield.callStatic.price(); + console.log('bb_cUSDC price after commit:', formatEther(afterCommitJTokenPrice)); + + const strategyBalanceAfterCommit = await strategy.callStatic.syncBalance(); + // debt share at block-number 12400000 + // arg is (1 - fee) / afterCommitJTokenPrice + const afterCommitForfeits = await strategy.callStatic.calForfeits(parseUnits('0.959', 6)); + const errorByJToknPrice = parseUnits('0.000002', 6); // price difference when commit/uncommit + + expect(strategyBalanceAfterCommit.sub(strategyBalanceBeforeCommit) + .gte(commitAmount.sub(fee).sub(afterCommitForfeits).sub(errorByJToknPrice))).to.be.true; + expect(strategyBalanceAfterCommit.sub(strategyBalanceBeforeCommit) + .lte(commitAmount.sub(fee).sub(afterCommitForfeits).add(errorByJToknPrice))).to.be.true; + console.log('Strategy USDC balance after commit:', formatUnits(strategyBalanceAfterCommit, 6)); + + const controllerBalanceAfterCommit = await usdc.balanceOf(deployerSigner.address); + expect(controllerBalanceBeforeCommit.sub(controllerBalanceAfterCommit).eq(commitAmount)).to.be.true; + console.log('Controller USDC balance after commit:', formatUnits(controllerBalanceAfterCommit, 6)); + + console.log('===== Uncommit 0.5 USDC ====='); + const uncommitAmount = parseUnits('0.5', 6); + const uncommitGas = await strategy.estimateGas.aggregateUncommit(uncommitAmount); + expect(uncommitGas.lte(800000)).to.be.true; + const uncommitTx = await strategy.aggregateUncommit(uncommitAmount, { gasLimit: 800000 }); + await uncommitTx.wait(); + + const afterUncommitJTokenPrice = await smartYield.callStatic.price(); + console.log('bb_cUSDC price after commit:', formatEther(afterUncommitJTokenPrice)); + + const strategyBalanceAfterUncommit = await strategy.callStatic.syncBalance(); + // debt share at block-number 12400000 + // arg is 0.5 / afterUncommitJTokenPrice + const afterUncommitForfeits = await strategy.callStatic.calForfeits(parseUnits('0.482', 6)); + expect(strategyBalanceAfterCommit.sub(strategyBalanceAfterUncommit) + .gte(uncommitAmount.sub(afterUncommitForfeits).sub(errorByJToknPrice))).to.be.true; + expect(strategyBalanceAfterCommit.sub(strategyBalanceAfterUncommit) + .lte(uncommitAmount.sub(afterUncommitForfeits).add(errorByJToknPrice))).to.be.true; + console.log('Strategy USDC balance after uncommit:', formatUnits(strategyBalanceAfterUncommit, 6)); + + const controllerBalanceAfterUncommit = await usdc.balanceOf(deployerSigner.address); + expect(controllerBalanceAfterUncommit.sub(controllerBalanceAfterCommit) + .gte(uncommitAmount.sub(afterUncommitForfeits).sub(errorByJToknPrice))).to.be.true; + expect(controllerBalanceAfterUncommit.sub(controllerBalanceAfterCommit) + .lte(uncommitAmount.sub(afterUncommitForfeits).add(errorByJToknPrice))).to.be.true; + console.log('Controller USDC balance after uncommit:', formatUnits(controllerBalanceAfterUncommit, 6)); + + console.log('===== Optional harvest ====='); + try { + // Send some BOND to strategy + const bond = ERC20__factory.connect(process.env.BARN_BRIDGE_BOND as string, deployerSigner); + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [process.env.BARN_BRIDGE_BOND_FUNDER as string] + }); + await ( + await bond + .connect(await ethers.getSigner(process.env.BARN_BRIDGE_BOND_FUNDER as string)) + .transfer(strategy.address, parseEther('0.01')) + ).wait(); + const harvestGas = await strategy.estimateGas.harvest(); + if (harvestGas.lte(1000000)) { + const harvestTx = await strategy.harvest({ gasLimit: 1000000 }); + await harvestTx.wait(); + const strategyBalanceAfterHarvest = await strategy.callStatic.syncBalance(); + expect(strategyBalanceAfterHarvest.gte(strategyBalanceAfterUncommit)).to.be.true; + console.log('Strategy USDC balance after harvest:', formatUnits(strategyBalanceAfterHarvest, 6)); + } + } catch (e) { + console.log('Cannot harvest:', e); + } + }); +});