From 160a3671cc5fde5fe187efd81fe75c5518f8a6a5 Mon Sep 17 00:00:00 2001 From: Beanstalk Farms <88561107+BeanstalkFarms@users.noreply.github.com> Date: Sun, 5 Dec 2021 21:13:05 -0800 Subject: [PATCH] bip-6: Soil Efficiency (#14) --- protocol/contracts/C.sol | 12 +- .../farm/facets/FieldFacet/Dibbler.sol | 3 +- .../farm/facets/SeasonFacet/Life.sol | 49 +++-- .../contracts/farm/facets/SeasonFacet/Sun.sol | 2 +- .../mocks/mockFacets/MockFieldFacet.sol | 25 --- .../mocks/mockFacets/MockSeasonFacet.sol | 28 +++ protocol/coverage_data/sun.json | 13 +- protocol/test/Sun.test.js | 170 +++++++++++++++++- 8 files changed, 250 insertions(+), 52 deletions(-) diff --git a/protocol/contracts/C.sol b/protocol/contracts/C.sol index 0b1ac6958..e7e68a125 100644 --- a/protocol/contracts/C.sol +++ b/protocol/contracts/C.sol @@ -56,8 +56,8 @@ library C { uint256 private constant ROOTS_BASE = 1e12; // Field - uint256 private constant SOIL_MAX_RATIO_CAP = 25e16; // 25% - uint256 private constant SOIL_MIN_RATIO_CAP = 1e15; // 0.1% + uint256 private constant MAX_SOIL_DENOMINATOR = 4; // 25% + uint256 private constant COMPLEX_WEATHER_DENOMINATOR = 1000; // 0.1% /** @@ -104,12 +104,12 @@ library C { return WITHDRAW_TIME; } - function getMinSoilRatioCap() internal pure returns (uint256) { - return SOIL_MIN_RATIO_CAP; + function getComplexWeatherDenominator() internal pure returns (uint256) { + return COMPLEX_WEATHER_DENOMINATOR; } - function getMaxSoilRatioCap() internal pure returns (uint256) { - return SOIL_MAX_RATIO_CAP; + function getMaxSoilDenominator() internal pure returns (uint256) { + return MAX_SOIL_DENOMINATOR; } function getHarvestPercentage() internal pure returns (uint256) { diff --git a/protocol/contracts/farm/facets/FieldFacet/Dibbler.sol b/protocol/contracts/farm/facets/FieldFacet/Dibbler.sol index deae3f8a4..5a40f9ec2 100644 --- a/protocol/contracts/farm/facets/FieldFacet/Dibbler.sol +++ b/protocol/contracts/farm/facets/FieldFacet/Dibbler.sol @@ -64,8 +64,7 @@ contract Dibbler { function saveSowTime() private { uint256 totalBeanSupply = bean().totalSupply(); - uint256 minTotalSoil = C.getMinSoilRatioCap().mul(bean().totalSupply()).div(1e18); - if (s.f.soil >= minTotalSoil) return; + if (totalSoil() >= totalBeanSupply.div(C.getComplexWeatherDenominator())) return; uint256 sowTime = block.timestamp.sub(s.season.timestamp); s.w.nextSowTime = uint32(sowTime); diff --git a/protocol/contracts/farm/facets/SeasonFacet/Life.sol b/protocol/contracts/farm/facets/SeasonFacet/Life.sol index da0411b8b..59c7878a9 100644 --- a/protocol/contracts/farm/facets/SeasonFacet/Life.sol +++ b/protocol/contracts/farm/facets/SeasonFacet/Life.sol @@ -106,33 +106,42 @@ contract Life { **/ function increaseSoil(uint256 amount) internal returns (int256) { - uint256 maxTotalSoil = C.getMaxSoilRatioCap().mul(bean().totalSupply()).div(1e18); - uint256 minTotalSoil = C.getMinSoilRatioCap().mul(bean().totalSupply()).div(1e18); - if (s.f.soil > maxTotalSoil) { - amount = s.f.soil.sub(maxTotalSoil); + uint256 maxTotalSoil = getMaxSoil(); + uint256 soil = s.f.soil; + if (soil > maxTotalSoil) { + amount = soil.sub(maxTotalSoil); decrementTotalSoil(amount); return -int256(amount); } - uint256 newTotalSoil = s.f.soil + amount; - amount = newTotalSoil <= maxTotalSoil ? amount : maxTotalSoil.sub(s.f.soil); - amount = newTotalSoil >= minTotalSoil ? amount : minTotalSoil.sub(s.f.soil); - + uint256 newTotalSoil = soil.add(amount); + if (newTotalSoil > maxTotalSoil) amount = maxTotalSoil.sub(soil); incrementTotalSoil(amount); return int256(amount); } - function decreaseSoil(uint256 amount) internal { + function decreaseSoil(uint256 amount, uint256 harvested) internal returns (int256) { + uint256 minTotalSoil = getMinSoil(harvested); + uint256 soil = s.f.soil; + if (soil < minTotalSoil) { + amount = minTotalSoil.sub(soil); + incrementTotalSoil(amount); + return int256(amount); + } + if (amount > soil) { + amount = soil.sub(minTotalSoil); + } else { + uint256 newTotalSoil = soil.sub(amount); + uint256 maxTotalSoil = getMaxSoil(); + if (newTotalSoil > maxTotalSoil) amount = soil.sub(maxTotalSoil); + else if (newTotalSoil < minTotalSoil) amount = soil.sub(minTotalSoil); + } + decrementTotalSoil(amount); + return -int256(amount); } function ensureSoilBounds() internal returns (int256) { - uint256 minTotalSoil = C.getMinSoilRatioCap().mul(bean().totalSupply()).div(1e18); - if (s.f.soil < minTotalSoil) { - uint256 amount = minTotalSoil.sub(s.f.soil); - incrementTotalSoil(amount); - return int256(amount); - } - uint256 maxTotalSoil = C.getMaxSoilRatioCap().mul(bean().totalSupply()).div(1e18); + uint256 maxTotalSoil = getMaxSoil(); if (s.f.soil > maxTotalSoil) { uint256 amount = s.f.soil.sub(maxTotalSoil); decrementTotalSoil(amount); @@ -141,6 +150,14 @@ contract Life { return 0; } + function getMaxSoil() internal view returns (uint256 maxSoil) { + maxSoil = bean().totalSupply().div(C.getMaxSoilDenominator()); + } + + function getMinSoil(uint256 amount) internal view returns (uint256 minSoil) { + minSoil = amount.mul(100).div(100 + s.w.yield); + } + function incrementTotalSoil(uint256 amount) internal { s.f.soil = s.f.soil.add(amount); } diff --git a/protocol/contracts/farm/facets/SeasonFacet/Sun.sol b/protocol/contracts/farm/facets/SeasonFacet/Sun.sol index c42be58ca..0e3e914ad 100644 --- a/protocol/contracts/farm/facets/SeasonFacet/Sun.sol +++ b/protocol/contracts/farm/facets/SeasonFacet/Sun.sol @@ -68,7 +68,7 @@ contract Sun is Weather { function growSupply(uint256 beans, uint256 price) private returns (uint256) { (uint256 newHarvestable, uint256 newSilo) = increaseSupply(beans); - int256 newSoil = ensureSoilBounds(); + int256 newSoil = decreaseSoil(beans, newHarvestable); emit SupplyIncrease(season(), price, newHarvestable, newSilo, newSoil); return newSilo; } diff --git a/protocol/contracts/mocks/mockFacets/MockFieldFacet.sol b/protocol/contracts/mocks/mockFacets/MockFieldFacet.sol index 167a53bbb..5f249bebf 100644 --- a/protocol/contracts/mocks/mockFacets/MockFieldFacet.sol +++ b/protocol/contracts/mocks/mockFacets/MockFieldFacet.sol @@ -16,15 +16,6 @@ contract MockFieldFacet is FieldFacet { using SafeMath for uint256; - function incrementTotalSoilE(uint256 amount) public { - incrementTotalSoil(amount); - ensureSoilBounds(); - } - - function incrementTotalSoilEE(uint256 amount) public { - incrementTotalSoil(amount); - } - function incrementTotalHarvestableE(uint256 amount) public { bean().mint(address(this), amount); s.f.harvestable = s.f.harvestable.add(amount); @@ -52,20 +43,4 @@ contract MockFieldFacet is FieldFacet { s.f.soil = s.f.soil.add(amount); } - function ensureSoilBounds() internal returns (int256) { - uint256 minTotalSoil = C.getMinSoilRatioCap().mul(bean().totalSupply()).div(100); - if (s.f.soil < minTotalSoil) { - uint256 amount = minTotalSoil.sub(s.f.soil); - incrementTotalSoil(amount); - return int256(amount); - } - uint256 maxTotalSoil = C.getMaxSoilRatioCap().mul(bean().totalSupply()).div(100); - if (s.f.soil > maxTotalSoil) { - uint256 amount = s.f.soil.sub(maxTotalSoil); - s.f.soil = s.f.soil.sub(amount, "MockField: Not enough Soil."); - return -int256(amount); - } - return 0; - } - } diff --git a/protocol/contracts/mocks/mockFacets/MockSeasonFacet.sol b/protocol/contracts/mocks/mockFacets/MockSeasonFacet.sol index 19941b5c2..380fd5f12 100644 --- a/protocol/contracts/mocks/mockFacets/MockSeasonFacet.sol +++ b/protocol/contracts/mocks/mockFacets/MockSeasonFacet.sol @@ -138,6 +138,34 @@ contract MockSeasonFacet is SeasonFacet { s.w.lastSoilPercent = number; } + function incrementTotalSoilE(uint256 amount) public { + incrementTotalSoil(amount); + } + + function decrementTotalSoilE(uint256 amount) public { + decrementTotalSoil(amount); + } + + function increaseSoilE(uint256 amount) public { + increaseSoil(amount); + } + + function decreaseSoilE(uint256 amount, uint256 harvested) public { + decreaseSoil(amount, harvested); + } + + function ensureSoilBoundsE() public { + ensureSoilBounds(); + } + + function minSoil(uint256 amount) public view returns (uint256) { + return getMinSoil(amount); + } + + function maxSoil() public view returns (uint256) { + return getMaxSoil(); + } + function resetAccount(address account) public { uint32 _s = season(); for (uint32 j = 0; j <= _s; j++) { diff --git a/protocol/coverage_data/sun.json b/protocol/coverage_data/sun.json index 14aef661f..89043d5df 100644 --- a/protocol/coverage_data/sun.json +++ b/protocol/coverage_data/sun.json @@ -1 +1,12 @@ -{"columns":["season","beansInSilo","harvestablePods","unripenedPods","soil","twapBeans","twapUSDC","divisor","beansInPool","ethInPool","newTotalSupply","newBeansInSilo","newSupplyofSoil","newHarvestablePods","newUnripenedPods","newTotalPods","supplyIncrease","seasonOfLastSupplyIncrease","currentSeason","currentTWAP","deltaHarvestablePods","deltaBeansInSilo","deltaSoil"],"data":[["1","1","0","0","0","8","10","10000","1","110","2","1","0","0","0","0","0","0","1","800000000000000000","0","0","0"],["1","1","0","0","0","12","10","10000","1","110","31","30","0","0","0","0","29","1","1","1200000000000000000","0","29","0"],["1","1","0","0","100","12","10","10000","1","110","31","30","7","0","0","0","29","1","1","1200000000000000000","0","29","-93"],["1","1","0","1","100","12","10","10000","1","110","31","29","7","1","0","1","28","1","1","1200000000000000000","1","28","-93"],["1","1","0","1","1","12","10","10000","10000","110","12891","2890","12","1","0","1","2889","1","1","1200000000000000000","1","2889","11"]]} \ No newline at end of file +{"columns": + ["season","beansInSilo","harvestablePods","unripenedPods","soil","twapBeans","twapUSDC","divisor","beansInPool","ethInPool","newTotalSupply","newBeansInSilo","newSupplyofSoil","newHarvestablePods","newUnripenedPods","newTotalPods","supplyIncrease","seasonOfLastSupplyIncrease","currentSeason","currentTWAP","deltaHarvestablePods","deltaBeansInSilo","deltaSoil"], + "data":[ + ["1","1","0","0","0","8","10","10000","1","110","2","1","0","0","0","0","0","0","1","800000000000000000","0","0","0"], + ["1","1","0","0","2","8","10","10000","1","110","2","1","0","0","0","0","0","0","1","800000000000000000","0","0","-2"], + ["1","1","0","0","0","12","10","10000","1","110","31","30","0","0","0","0","29","1","1","1200000000000000000","0","29","0"], + ["1","1","0","0","100","12","10","10000","1","110","31","30","7","0","0","0","29","1","1","1200000000000000000","0","29","-93"], + ["1","1","0","1","100","12","10","10000","1","110","31","29","7","1","0","1","28","1","1","1200000000000000000","1","28","-93"], + ["1","1","0","2","1","12","10","10000","10000","110","12891","2889","2","2","0","2","2889","1","1","1200000000000000000","2","2888","1"], + ["1","1","0","2","3000","12","10","10000","10000","110","12891","2889","110","2","0","2","2889","1","1","1200000000000000000","2","2888","-2890"] + ] +} \ No newline at end of file diff --git a/protocol/test/Sun.test.js b/protocol/test/Sun.test.js index aa407fbf2..bc4d5c2af 100644 --- a/protocol/test/Sun.test.js +++ b/protocol/test/Sun.test.js @@ -31,6 +31,7 @@ describe('Sun', function () { describe(testStr.concat((v)), function () { testData = {} columns.forEach((key, i) => testData[key] = tests[v][i]) + console.log(testData); before(async function () { await this.season.resetState() await this.pair.burnBeans(this.bean.address) @@ -43,7 +44,7 @@ describe('Sun', function () { await this.bean.mint(this.silo.address, this.testData.beansInSilo) await this.bean.mint(this.pair.address, this.testData.beansInPool) await this.silo.incrementDepositedBeansE(this.testData.beansInSilo) - await this.field.incrementTotalSoilEE(this.testData.soil) + await this.season.incrementTotalSoilE(this.testData.soil) await this.silo.depositSiloAssetsE(userAddress, '1', '100000') await this.field.incrementTotalPodsE((parseInt(this.testData.unripenedPods) + parseInt(this.testData.harvestablePods)).toString()) await this.field.incrementTotalHarvestableE(this.testData.harvestablePods) @@ -83,3 +84,170 @@ describe('Sun', function () { }) }) }) + + +describe('Sun Soil', function () { + + before(async function () { + [owner,user,user2] = await ethers.getSigners() + userAddress = user.address + user2Address = user2.address + const contracts = await deploy("Test", false, true) + ownerAddress = contracts.account + this.diamond = contracts.beanstalkDiamond + this.season = await ethers.getContractAt('MockSeasonFacet', this.diamond.address) + this.field = await ethers.getContractAt('MockFieldFacet', this.diamond.address) + this.silo = await ethers.getContractAt('MockSiloFacet', this.diamond.address) + this.bean = await ethers.getContractAt('MockToken', contracts.bean) + this.pair = await ethers.getContractAt('MockUniswapV2Pair', contracts.pair) + this.pegPair = await ethers.getContractAt('MockUniswapV2Pair', contracts.pegPair) + await this.bean.mint(this.silo.address, '100000') + await this.season.setYieldE('100') + }); + + this.beforeEach(async function () { + await this.season.decrementTotalSoilE(await this.field.totalSoil()) + }) + + it("Properly sets the soil bounds", async function () { + expect(await this.season.minSoil('100')).to.be.equal('50') + expect(await this.season.maxSoil()).to.be.equal('25000') + }); + + // Increase Soil + + describe("Increase above max Soil", async function () { + beforeEach(async function () { + await this.season.increaseSoilE('100000') + }); + + it("Properly sets the soil", async function () { + expect(await this.field.totalSoil()).to.be.equal('25000') + }); + }); + + describe("Increase when already above above max Soil", async function () { + beforeEach(async function () { + await this.season.incrementTotalSoilE('100000') + await this.season.increaseSoilE('100000') + }); + + it("Properly sets the soil", async function () { + expect(await this.field.totalSoil()).to.be.equal('25000') + }); + }); + + describe("Increase little Soil", async function () { + beforeEach(async function () { + await this.season.increaseSoilE('1') + }); + + it("Properly sets the soil", async function () { + expect(await this.field.totalSoil()).to.be.equal('1') + }); + }); + + describe("Increase to normal Soil", async function () { + beforeEach(async function () { + await this.season.increaseSoilE('100') + }); + + it("Properly sets the soil", async function () { + expect(await this.field.totalSoil()).to.be.equal('100') + }); + }); + + // Decrease Soil + + describe("Decrease above max Soil", async function () { + beforeEach(async function () { + await this.season.incrementTotalSoilE('100000') + await this.season.decreaseSoilE('1', '100') + }); + + it("Properly sets the max soil", async function () { + expect(await this.field.totalSoil()).to.be.equal('25000') + }); + }); + + describe("Decrease already below min Soil", async function () { + beforeEach(async function () { + await this.season.incrementTotalSoilE('2') + await this.season.decreaseSoilE('1', '100') + }); + + it("Properly sets the soil", async function () { + expect(await this.field.totalSoil()).to.be.equal('50') + }); + }); + + describe("Decrease below min Soil", async function () { + beforeEach(async function () { + await this.season.incrementTotalSoilE('110') + await this.season.decreaseSoilE('100', '100') + }); + + it("Properly sets the soil", async function () { + expect(await this.field.totalSoil()).to.be.equal('50') + }); + }); + + describe("Decrease to normal Soil", async function () { + beforeEach(async function () { + await this.season.incrementTotalSoilE('110') + await this.season.decreaseSoilE('10', '100') + }); + + it("Properly sets the soil", async function () { + expect(await this.field.totalSoil()).to.be.equal('100') + }); + }); + + describe("Decrease below 0 Soil", async function () { + beforeEach(async function () { + await this.season.incrementTotalSoilE('100') + await this.season.decreaseSoilE('110', '10') + }); + + it("Properly sets the soil", async function () { + expect(await this.field.totalSoil()).to.be.equal('5') + }); + }); + + describe("Decrease to 0 Soil", async function () { + beforeEach(async function () { + await this.season.incrementTotalSoilE('100') + await this.season.decreaseSoilE('110', '0') + }); + + it("Properly sets the soil", async function () { + expect(await this.field.totalSoil()).to.be.equal('0') + }); + }); + + // Ensure soil bounds + + // Decrease Soil + + describe("When above max soil", async function () { + beforeEach(async function () { + await this.season.incrementTotalSoilE('100000') + await this.season.ensureSoilBoundsE() + }); + + it("Properly sets the max soil", async function () { + expect(await this.field.totalSoil()).to.be.equal('25000') + }); + }); + + describe("When normal soil", async function () { + beforeEach(async function () { + await this.season.incrementTotalSoilE('2') + await this.season.ensureSoilBoundsE() + }); + + it("Properly sets the soil", async function () { + expect(await this.field.totalSoil()).to.be.equal('2') + }); + }); +});