diff --git a/protocol/abi/Beanstalk.json b/protocol/abi/Beanstalk.json index 175157c655..5f4e26d711 100644 --- a/protocol/abi/Beanstalk.json +++ b/protocol/abi/Beanstalk.json @@ -4375,9 +4375,9 @@ }, { "indexed": false, - "internalType": "int128", + "internalType": "int96", "name": "stem", - "type": "int128" + "type": "int96" }, { "indexed": false, @@ -5692,7 +5692,7 @@ "type": "address" } ], - "name": "getLastStem", + "name": "getLastMowedStem", "outputs": [ { "internalType": "int96", @@ -6025,9 +6025,9 @@ "name": "seasonToStem", "outputs": [ { - "internalType": "int128", + "internalType": "int96", "name": "stem", - "type": "int128" + "type": "int96" } ], "stateMutability": "view", @@ -6057,9 +6057,9 @@ "name": "stemTipForToken", "outputs": [ { - "internalType": "int128", + "internalType": "int96", "name": "_stemTip", - "type": "int128" + "type": "int96" } ], "stateMutability": "view", diff --git a/protocol/contracts/beanstalk/init/InitDiamond.sol b/protocol/contracts/beanstalk/init/InitDiamond.sol index fe64fbde64..b7b5c54d25 100644 --- a/protocol/contracts/beanstalk/init/InitDiamond.sol +++ b/protocol/contracts/beanstalk/init/InitDiamond.sol @@ -59,6 +59,7 @@ contract InitDiamond { s.season.withdrawSeasons = 25; s.season.period = C.getSeasonPeriod(); s.season.timestamp = block.timestamp; + s.season.stemStartSeason = 0; s.season.start = s.season.period > 0 ? (block.timestamp / s.season.period) * s.season.period : block.timestamp; diff --git a/protocol/contracts/ecosystem/price/BeanstalkPrice.sol b/protocol/contracts/ecosystem/price/BeanstalkPrice.sol index c2b8efff55..b580217850 100644 --- a/protocol/contracts/ecosystem/price/BeanstalkPrice.sol +++ b/protocol/contracts/ecosystem/price/BeanstalkPrice.sol @@ -3,8 +3,10 @@ pragma solidity =0.7.6; pragma experimental ABIEncoderV2; import "./CurvePrice.sol"; +import {WellPrice, C, SafeMath} from "./WellPrice.sol"; -contract BeanstalkPrice is CurvePrice { +contract BeanstalkPrice is CurvePrice, WellPrice { + using SafeMath for uint256; struct Prices { uint256 price; @@ -14,15 +16,16 @@ contract BeanstalkPrice is CurvePrice { } function price() external view returns (Prices memory p) { - p.ps = new P.Pool[](1); + p.ps = new P.Pool[](2); p.ps[0] = getCurve(); + p.ps[1] = getConstantProductWell(C.BEAN_ETH_WELL); - + // assumes that liquidity and prices on all pools uses the same precision. for (uint256 i = 0; i < p.ps.length; i++) { - p.price += p.ps[i].price * p.ps[i].liquidity; + p.price += p.ps[i].price.mul(p.ps[i].liquidity); p.liquidity += p.ps[i].liquidity; p.deltaB += p.ps[i].deltaB; } - p.price /= p.liquidity; + p.price = p.price.div(p.liquidity); } } \ No newline at end of file diff --git a/protocol/contracts/ecosystem/price/WellPrice.sol b/protocol/contracts/ecosystem/price/WellPrice.sol new file mode 100644 index 0000000000..2e61480edf --- /dev/null +++ b/protocol/contracts/ecosystem/price/WellPrice.sol @@ -0,0 +1,80 @@ +//SPDX-License-Identifier: MIT +pragma solidity =0.7.6; +pragma experimental ABIEncoderV2; + +import {P} from "./P.sol"; +import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol"; +import {IWell, IERC20} from "../../interfaces/basin/IWell.sol"; +import {IBeanstalkWellFunction} from "../../interfaces/basin/IBeanstalkWellFunction.sol"; +import {LibUsdOracle} from "../../libraries/Oracle/LibUsdOracle.sol"; +import {LibWellMinting} from "../../libraries/Minting/LibWellMinting.sol"; +import {LibWell} from "../../libraries/Well/LibWell.sol"; +import {C} from "../../C.sol"; + +interface IBEANSTALK { + function bdv(address token, uint256 amount) external view returns (uint256); + + function poolDeltaB(address pool) external view returns (int256); +} + +interface dec{ + function decimals() external view returns (uint256); +} + +contract WellPrice { + + using SafeMath for uint256; + + address private constant BEANSTALK = 0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5; + uint256 private constant WELL_DECIMALS = 1e18; + uint256 private constant PRICE_PRECISION = 1e6; + + struct Pool { + address pool; + address[2] tokens; + uint256[2] balances; + uint256 price; + uint256 liquidity; + int256 deltaB; + uint256 lpUsd; + uint256 lpBdv; + } + + function getConstantProductWell(address wellAddress) public view returns (P.Pool memory pool) { + IWell well = IWell(wellAddress); + pool.pool = wellAddress; + + IERC20[] memory wellTokens = well.tokens(); + pool.tokens = [address(wellTokens[0]), address(wellTokens[1])]; + + { + uint256[] memory wellBalances = well.getReserves(); + pool.balances = [wellBalances[0], wellBalances[1]]; + } + + uint256 beanIndex = LibWell.getBeanIndex(wellTokens); + uint256 tknIndex = beanIndex == 0 ? 1 : 0; + + // swap 1 bean of the opposite asset to get the usd price + // price = amtOut/tknOutPrice + pool.price = + well.getSwapOut(wellTokens[beanIndex], wellTokens[tknIndex], 1e6) // 1e18 + .mul(PRICE_PRECISION) // 1e6 + .div(LibUsdOracle.getUsdPrice(address(wellTokens[tknIndex]))); // 1e18 + + // liquidity is calculated by beanAmt * beanPrice * 2 + pool.liquidity = + pool.balances[beanIndex] // 1e6 + .mul(pool.price) // 1e6 + .div(PRICE_PRECISION) + .mul(2); + + pool.deltaB = IBEANSTALK(BEANSTALK).poolDeltaB(wellAddress); + + pool.lpUsd = pool.liquidity.mul(WELL_DECIMALS).div(IERC20(wellAddress).totalSupply()); + + pool.lpBdv = IBEANSTALK(BEANSTALK).bdv(wellAddress, WELL_DECIMALS); + + } + +} diff --git a/protocol/scripts/impersonate.js b/protocol/scripts/impersonate.js index 0892032029..f603ed405e 100644 --- a/protocol/scripts/impersonate.js +++ b/protocol/scripts/impersonate.js @@ -24,8 +24,10 @@ const { ETH_USDC_UNISWAP_V3, ETH_USDT_UNISWAP_V3, USDT, - ETH_USD_CHAINLINK_AGGREGATOR + ETH_USD_CHAINLINK_AGGREGATOR, + BEAN_ETH_WELL } = require('../test/utils/constants'); +const { deployWell } = require('../utils/well.js'); const { impersonateSigner, mintEth } = require('../utils'); const { getSigner } = '../utils' @@ -95,6 +97,8 @@ async function weth() { WETH, JSON.parse(tokenJson).deployedBytecode, ]); + const weth = await ethers.getContractAt("MockToken", WETH); + await weth.setDecimals(18); } async function router() { @@ -268,6 +272,15 @@ async function ethUsdtUniswap() { ]); } +async function beanEthWell() { + const well = await deployWell([BEAN, WETH]); + const bytecode = await ethers.provider.getCode(well.address) + await network.provider.send("hardhat_setCode", [ + BEAN_ETH_WELL, + bytecode, + ]); +} + async function ethUsdChainlinkAggregator() { let chainlinkAggregatorJson = fs.readFileSync(`./artifacts/contracts/mocks/chainlink/MockChainlinkAggregator.sol/MockChainlinkAggregator.json`); @@ -279,6 +292,8 @@ async function ethUsdChainlinkAggregator() { await ethUsdChainlinkAggregator.setDecimals(6) } + + exports.impersonateRouter = router exports.impersonateBean = bean exports.impersonateCurve = curve @@ -295,3 +310,4 @@ exports.impersonateEthUsdcUniswap = ethUsdcUniswap exports.impersonateEthUsdtUniswap = ethUsdtUniswap exports.impersonateBeanstalk = impersonateBeanstalk exports.impersonateEthUsdChainlinkAggregator = ethUsdChainlinkAggregator +exports.impersonateBeanEthWell = beanEthWell diff --git a/protocol/test/LegacyClaim.test.js b/protocol/test/LegacyClaim.test.js index 0d3078d718..309bb29189 100644 --- a/protocol/test/LegacyClaim.test.js +++ b/protocol/test/LegacyClaim.test.js @@ -9,8 +9,6 @@ const { upgradeWithNewFacets } = require("../scripts/diamond"); describe("Legacy Claim", async function () { before(async function () { - console.log("startup stuff"); - try { await network.provider.request({ method: "hardhat_reset", @@ -47,7 +45,6 @@ describe("Legacy Claim", async function () { this.diamond = BEANSTALK; - console.log("this.diamond: ", this.diamond); // this.season = await ethers.getContractAt("MockSeasonFacet", this.diamond); diff --git a/protocol/test/beanstalkPrice.test.js b/protocol/test/beanstalkPrice.test.js new file mode 100644 index 0000000000..98219b1ebb --- /dev/null +++ b/protocol/test/beanstalkPrice.test.js @@ -0,0 +1,383 @@ +const { expect } = require('chai'); +const { deploy } = require('../scripts/deploy.js') +const { EXTERNAL } = require('./utils/balances.js') +const { to18, to6, advanceTime } = require('./utils/helpers.js') +const { BEAN, BEANSTALK, BEAN_3_CURVE, THREE_CURVE, THREE_POOL, WETH, STABLE_FACTORY, BEAN_ETH_WELL } = require('./utils/constants') +const { takeSnapshot, revertToSnapshot } = require("./utils/snapshot"); +const { deployWell, setReserves, whitelistWell } = require('../utils/well.js'); +const { setEthUsdPrice, setEthUsdcPrice, setEthUsdtPrice } = require('../scripts/usdOracle.js'); +const { getBeanstalk } = require('../utils/contracts.js'); +const { impersonateBeanEthWell } = require('../scripts/impersonate.js') +const fs = require('fs'); + +let user, user2, owner; +let userAddress, ownerAddress, user2Address; + +describe('BeanstalkPrice', function () { + before(async function () { + + [owner, user] = await ethers.getSigners(); + const contracts = await deploy("Test", false, true); + ownerAddress = contracts.account; + userAddress = user.address; + this.diamond = contracts.beanstalkDiamond; + this.beanstalk = await getBeanstalk(this.diamond.address); + this.curve = await ethers.getContractAt('CurveFacet', this.diamond.address) + await impersonateBeanEthWell() + this.well = await ethers.getContractAt("IWell", BEAN_ETH_WELL); + this.wellToken = await ethers.getContractAt("IERC20", this.well.address) + this.threeCurve = await ethers.getContractAt('MockToken', THREE_CURVE) + this.threePool = await ethers.getContractAt('Mock3Curve', THREE_POOL) + this.beanThreeCurve = await ethers.getContractAt('MockMeta3Curve', BEAN_3_CURVE); + this.season = await ethers.getContractAt('MockSeasonFacet', this.diamond.address); + this.bean = await ethers.getContractAt("MockToken", BEAN); + + await this.bean.connect(user).approve(this.diamond.address, ethers.constants.MaxUint256) + await this.bean.connect(user).approve(this.beanThreeCurve.address, ethers.constants.MaxUint256); + await this.bean.mint(userAddress, to6('10000000000')) + + await this.threeCurve.mint(userAddress, to18('10000000000')) + await this.threePool.set_virtual_price(to18('1')) + await this.threeCurve.connect(user).approve(this.beanThreeCurve.address, ethers.constants.MaxUint256) + + await this.bean.mint(ownerAddress, to6('1000000000')) + await this.wellToken.connect(owner).approve(this.beanstalk.address, ethers.constants.MaxUint256) + await this.bean.connect(owner).approve(this.beanstalk.address, ethers.constants.MaxUint256) + + await this.beanThreeCurve.set_A_precise('1000') + await this.beanThreeCurve.set_virtual_price(ethers.utils.parseEther('1')) + await this.beanThreeCurve.connect(user).approve(this.threeCurve.address, ethers.constants.MaxUint256) + await this.beanThreeCurve.connect(user).approve(this.diamond.address, ethers.constants.MaxUint256) + await this.threeCurve.connect(user).approve(this.diamond.address, ethers.constants.MaxUint256) + + this.result = await this.curve.connect(user).addLiquidity( + BEAN_3_CURVE, + STABLE_FACTORY, + [to6('500000'), to18('500000')], + to18('0'), + EXTERNAL, + EXTERNAL + ) + + this.result = await this.curve.connect(user).addLiquidity( + BEAN_3_CURVE, + STABLE_FACTORY, + [to6('500000'), to18('500000')], + to18('0'), + EXTERNAL, + EXTERNAL + ) + + await setEthUsdPrice('999.998018') + await setEthUsdcPrice('1000') + await setEthUsdtPrice('1000') + + await setReserves( + owner, + this.well, + [to6('1000000'), to18('1000')] + ); + + await setReserves( + owner, + this.well, + [to6('1000000'), to18('1000')] + ); + + await whitelistWell(this.well.address, '10000', to6('4')); + await this.season.captureWellE(this.well.address); + + const BeanstalkPrice = await ethers.getContractFactory('BeanstalkPrice'); + const _beanstalkPrice = await BeanstalkPrice.deploy(); + await _beanstalkPrice.deployed(); + this.beanstalkPrice = await ethers.getContractAt('BeanstalkPrice', _beanstalkPrice.address); + + }); + + beforeEach(async function () { + snapshotId = await takeSnapshot(); + }); + + afterEach(async function () { + await revertToSnapshot(snapshotId); + }); + + describe("Price", async function () { + it('deltaB = 0', async function () { + const p = await this.beanstalkPrice.price() + // price is within +/- 1 due to curve rounding + expect(p.price).to.equal('999999'); + expect(p.liquidity).to.equal('3999997000000'); + expect(p.deltaB).to.be.eq('0'); + }) + + it('deltaB > 0, curve only', async function () { + this.result = await this.curve.connect(user).addLiquidity( + BEAN_3_CURVE, + STABLE_FACTORY, + [to6('0'), to18('100000')], + to18('0'), + EXTERNAL, + EXTERNAL + ) + const p = await this.beanstalkPrice.price() + const c = await this.beanstalkPrice.getCurve() + const w = await this.beanstalkPrice.getConstantProductWell(this.well.address) + + expect(p.price).to.equal('1004479'); + expect(p.liquidity).to.equal('4108727000000'); + expect(p.deltaB).to.equal('49891561002'); + + expect(c.price).to.equal('1008729'); + expect(c.liquidity).to.equal('2108729000000'); + expect(c.deltaB).to.equal('49891561002'); + + expect(w.price).to.equal('999999'); + expect(w.liquidity).to.equal('1999998000000'); + expect(w.deltaB).to.equal('0'); + }) + + it('deltaB > 0, wells only', async function () { + await advanceTime(1800) + await setReserves( + owner, + this.well, + [to6('500000'), to18('1000')] + ); + await advanceTime(1800) + await user.sendTransaction({ + to: beanstalk.address, + value: 0 + }) + + const p = await this.beanstalkPrice.price() + const c = await this.beanstalkPrice.getCurve() + const w = await this.beanstalkPrice.getConstantProductWell(this.well.address) + + expect(p.price).to.equal('1499997'); + expect(p.liquidity).to.equal('3999995000000'); + expect(p.deltaB).to.equal('133679332828'); + + expect(c.price).to.equal('999999'); + expect(c.liquidity).to.equal('1999999000000'); + expect(c.deltaB).to.equal('0'); + + expect(w.price).to.equal('1999996'); + expect(w.liquidity).to.equal('1999996000000'); + expect(w.deltaB).to.equal('133679332828'); + }) + + it('deltaB > 0, wells and curve', async function () { + this.result = await this.curve.connect(user).addLiquidity( + BEAN_3_CURVE, + STABLE_FACTORY, + [to6('0'), to18('100000')], + to18('0'), + EXTERNAL, + EXTERNAL + ) + await advanceTime(1800) + await setReserves( + owner, + this.well, + [to6('500000'), to18('1000')] + ); + await advanceTime(1800) + await user.sendTransaction({ + to: beanstalk.address, + value: 0 + }) + + const p = await this.beanstalkPrice.price() + const c = await this.beanstalkPrice.getCurve() + const w = await this.beanstalkPrice.getConstantProductWell(this.well.address) + + expect(p.price).to.equal('1491246'); + expect(p.liquidity).to.equal('4108725000000'); + expect(p.deltaB).to.equal('183543345294'); + + expect(c.price).to.equal('1008729'); + expect(c.liquidity).to.equal('2108729000000'); + expect(c.deltaB).to.equal('49891561002'); + + expect(w.price).to.equal('1999996'); + expect(w.liquidity).to.equal('1999996000000'); + expect(w.deltaB).to.equal('133651784292'); + }) + + it('deltaB < 0, curve only', async function () { + this.result = await this.curve.connect(user).addLiquidity( + BEAN_3_CURVE, + STABLE_FACTORY, + [to6('100000'), to18('0')], + to18('0'), + EXTERNAL, + EXTERNAL + ) + + // ~500 beans need be to be bought to get back to peg + const p = await this.beanstalkPrice.price() + const c = await this.beanstalkPrice.getCurve() + const w = await this.beanstalkPrice.getConstantProductWell(this.well.address) + + expect(p.price).to.equal('995576'); + expect(p.liquidity).to.equal('4090478600000'); + expect(p.deltaB).to.equal('-50108438998'); + + expect(c.price).to.equal('991346'); + expect(c.liquidity).to.equal('2090480600000'); + expect(c.deltaB).to.equal('-50108438998'); + + expect(w.price).to.equal('999999'); + expect(w.liquidity).to.equal('1999998000000'); + expect(w.deltaB).to.equal('0'); + + }) + + it('deltaB < 0, wells only', async function () { + await advanceTime(1800) + await setReserves( + owner, + this.well, + [to6('2000000'), to18('1000')] + ); + await advanceTime(1800) + await user.sendTransaction({ + to: beanstalk.address, + value: 0 + }) + + const p = await this.beanstalkPrice.price() + const c = await this.beanstalkPrice.getCurve() + const w = await this.beanstalkPrice.getConstantProductWell(this.well.address) + + expect(p.price).to.equal('749999'); + expect(p.liquidity).to.equal('3999995000000'); + expect(p.deltaB).to.equal('-224612602483'); + + expect(c.price).to.equal('999999'); + expect(c.liquidity).to.equal('1999999000000'); + expect(c.deltaB).to.equal('0'); + + expect(w.price).to.equal('499999'); + expect(w.liquidity).to.equal('1999996000000'); + expect(w.deltaB).to.equal('-224612602483'); + }) + + it('deltaB < 0, wells and curve', async function () { + this.result = await this.curve.connect(user).addLiquidity( + BEAN_3_CURVE, + STABLE_FACTORY, + [to6('100000'), to18('0')], + to18('0'), + EXTERNAL, + EXTERNAL + ) + await advanceTime(1800) + await setReserves( + owner, + this.well, + [to6('2000000'), to18('1000')] + ); + await advanceTime(1800) + await user.sendTransaction({ + to: beanstalk.address, + value: 0 + }) + + const p = await this.beanstalkPrice.price() + const c = await this.beanstalkPrice.getCurve() + const w = await this.beanstalkPrice.getConstantProductWell(this.well.address) + + expect(p.price).to.equal('751106'); + expect(p.liquidity).to.equal('4090476600000'); + expect(p.deltaB).to.be.within('-274563881303', '-274485381948') + + expect(c.price).to.equal('991346'); + expect(c.liquidity).to.equal('2090480600000'); + expect(c.deltaB).to.equal('-50108438998'); + + expect(w.price).to.equal('499999'); + expect(w.liquidity).to.equal('1999996000000'); + expect(w.deltaB).to.equal('-224376942950'); + }) + + it('well deltaB > 0, curve deltaB < 0', async function () { + this.result = await this.curve.connect(user).addLiquidity( + BEAN_3_CURVE, + STABLE_FACTORY, + [to6('100000'), to18('0')], + to18('0'), + EXTERNAL, + EXTERNAL + ) + await advanceTime(1800) + await setReserves( + owner, + this.well, + [to6('500000'), to18('1000')] + ); + await advanceTime(1800) + await user.sendTransaction({ + to: beanstalk.address, + value: 0 + }) + + const p = await this.beanstalkPrice.price() + const c = await this.beanstalkPrice.getCurve() + const w = await this.beanstalkPrice.getConstantProductWell(this.well.address) + + expect(p.price).to.equal('1484514'); + expect(p.liquidity).to.equal('4090476600000'); + expect(p.deltaB).to.equal('83488280312'); + + expect(c.price).to.equal('991346'); + expect(c.liquidity).to.equal('2090480600000'); + expect(c.deltaB).to.equal('-50108438998'); + + expect(w.price).to.equal('1999996'); + expect(w.liquidity).to.equal('1999996000000'); + expect(w.deltaB).to.be.within('133569202852','133596719310'); + }) + + it('well deltaB < 0, curve deltaB > 0', async function () { + this.result = await this.curve.connect(user).addLiquidity( + BEAN_3_CURVE, + STABLE_FACTORY, + [to6('0'), to18('100000')], + to18('0'), + EXTERNAL, + EXTERNAL + ) + await advanceTime(1800) + await setReserves( + owner, + this.well, + [to6('2000000'), to18('1000')] + ); + await advanceTime(1800) + await user.sendTransaction({ + to: beanstalk.address, + value: 0 + }) + + const p = await this.beanstalkPrice.price() + const c = await this.beanstalkPrice.getCurve() + const w = await this.beanstalkPrice.getConstantProductWell(this.well.address) + + expect(p.price).to.equal('761095'); + expect(p.liquidity).to.equal('4108725000000'); + expect(p.deltaB).to.be.within('-174485381948','-174406936344'); + + expect(c.price).to.equal('1008729'); + expect(c.liquidity).to.equal('2108729000000'); + expect(c.deltaB).to.equal('49891561002'); + + expect(w.price).to.equal('499999'); + expect(w.liquidity).to.equal('1999996000000'); + expect(w.deltaB).to.equal('-224298497346'); + }) + + }); + +}); diff --git a/protocol/test/utils/constants.js b/protocol/test/utils/constants.js index 614e48a26b..20d6899b0b 100644 --- a/protocol/test/utils/constants.js +++ b/protocol/test/utils/constants.js @@ -46,5 +46,6 @@ module.exports = { DEPOT_DEPLOYER: '0x058a783D98cDBB78d403c6B613C17d6b96f20d06', ETH_USDT_UNISWAP_V3: '0x4e68Ccd3E89f51C3074ca5072bbAC773960dFa36', ETH_USD_CHAINLINK_AGGREGATOR: '0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419', - BEANSTALK_PUMP: '0xc4AD29ba4B3c580e6D59105FFf484999997675Ff' + BEANSTALK_PUMP: '0xc4AD29ba4B3c580e6D59105FFf484999997675Ff', + BEAN_ETH_WELL: '0x9bAaB117304f7D6517048e371025dB8f89a8DbE5' } diff --git a/protocol/test/utils/helpers.js b/protocol/test/utils/helpers.js index bdba8d9cf4..7d28cd74e9 100644 --- a/protocol/test/utils/helpers.js +++ b/protocol/test/utils/helpers.js @@ -42,6 +42,16 @@ function toX(amount, x) { return ethers.utils.parseUnits(amount,x); } +async function advanceTime(time) { + let timestamp = (await ethers.provider.getBlock('latest')).timestamp; + timestamp += time + await hre.network.provider.request({ + method: "evm_setNextBlockTimestamp", + params: [timestamp], + }); +} + + exports.toBean = toBean exports.toStalk = toStalk exports.toEther = toEther @@ -50,4 +60,5 @@ exports.to6 = to6 exports.parseJson = parseJson exports.getEthSpentOnGas = getEthSpentOnGas exports.incrementTime = incrementTime +exports.advanceTime = advanceTime exports.toX = toX \ No newline at end of file