From 87b7fb0e6b9310f8e4c4fb8b88431bb41a1af6de Mon Sep 17 00:00:00 2001 From: yaron velner Date: Wed, 23 Jun 2021 16:27:18 +0300 Subject: [PATCH 01/90] initial b.protocol commit --- .../contracts/contracts/B.Protocol/BAMM.sol | 218 + .../contracts/B.Protocol/ChainlinkTestnet.sol | 35 + .../contracts/B.Protocol/PriceFormula.sol | 50 + .../contracts/contracts/B.Protocol/crop.sol | 181 + .../contracts/test/B.Protocol/BAMMTest.js | 3869 +++++++++++++++++ .../test/B.Protocol/PriceFormulaTest.js | 31 + 6 files changed, 4384 insertions(+) create mode 100644 packages/contracts/contracts/B.Protocol/BAMM.sol create mode 100644 packages/contracts/contracts/B.Protocol/ChainlinkTestnet.sol create mode 100644 packages/contracts/contracts/B.Protocol/PriceFormula.sol create mode 100644 packages/contracts/contracts/B.Protocol/crop.sol create mode 100644 packages/contracts/test/B.Protocol/BAMMTest.js create mode 100644 packages/contracts/test/B.Protocol/PriceFormulaTest.js diff --git a/packages/contracts/contracts/B.Protocol/BAMM.sol b/packages/contracts/contracts/B.Protocol/BAMM.sol new file mode 100644 index 000000000..bc87c2424 --- /dev/null +++ b/packages/contracts/contracts/B.Protocol/BAMM.sol @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.11; + +import "./../StabilityPool.sol"; +import "./crop.sol"; +import "./PriceFormula.sol"; +import "./../Interfaces/IPriceFeed.sol"; +import "./../Dependencies/IERC20.sol"; +import "./../Dependencies/SafeMath.sol"; +import "./../Dependencies/Ownable.sol"; +import "./../Dependencies/AggregatorV3Interface.sol"; + +contract BAMM is CropJoin, PriceFormula, Ownable { + using SafeMath for uint256; + + AggregatorV3Interface public immutable priceAggregator; + StabilityPool public immutable SP; + IERC20 public immutable LUSD; + + address payable public immutable feePool; + uint public constant MAX_FEE = 100; // 1% + uint public fee = 0; // fee in bps + uint public A = 20; + uint public constant MIN_A = 20; + uint public constant MAX_A = 200; + + uint public immutable maxDiscount; // max discount in bips + + address public constant frontEndTag = address(0); + + uint constant PRECISION = 1e18; + + constructor(address _priceAggregator, address payable _SP, address _LUSD, address _LQTY, uint _maxDiscount, address payable _feePool) public + CropJoin(address(new Dummy()), "B.AMM", address(new DummyGem()), _LQTY) { + priceAggregator = AggregatorV3Interface(_priceAggregator); + SP = StabilityPool(_SP); + LUSD = IERC20(_LUSD); + + feePool = _feePool; + maxDiscount = _maxDiscount; + } + + function setParams(uint _A, uint _fee) external onlyOwner { + require(_fee <= MAX_FEE, "setParams: fee is too big"); + require(_A >= MIN_A, "setParams: A too small"); + require(_A <= MAX_A, "setParams: A too big"); + + fee = _fee; + A = _A; + } + + function fetchPrice() public view returns(uint) { + uint chainlinkDecimals; + uint chainlinkLatestAnswer; + uint chainlinkTimestamp; + + // First, try to get current decimal precision: + try priceAggregator.decimals() returns (uint8 decimals) { + // If call to Chainlink succeeds, record the current decimal precision + chainlinkDecimals = decimals; + } catch { + // If call to Chainlink aggregator reverts, return a zero response with success = false + return 0; + } + + // Secondly, try to get latest price data: + try priceAggregator.latestRoundData() returns + ( + uint80 /* roundId */, + int256 answer, + uint256 /* startedAt */, + uint256 timestamp, + uint80 /* answeredInRound */ + ) + { + // If call to Chainlink succeeds, return the response and success = true + chainlinkLatestAnswer = uint(answer); + chainlinkTimestamp = timestamp; + } catch { + // If call to Chainlink aggregator reverts, return a zero response with success = false + return 0; + } + + if(chainlinkTimestamp + 1 hours < now) return 0; // price is down + + uint chainlinkFactor = 10 ** chainlinkDecimals; + return chainlinkLatestAnswer.mul(PRECISION) / chainlinkFactor; + } + + function deposit(uint lusdAmount) external { + // update share + uint lusdValue = SP.getCompoundedLUSDDeposit(address(this)); + uint ethValue = SP.getDepositorETHGain(address(this)).add(address(this).balance); + + uint price = fetchPrice(); + require(ethValue == 0 || price > 0, "deposit: chainlink is down"); + + uint totalValue = lusdValue.add(ethValue.mul(price) / PRECISION); + + uint newShare = PRECISION; + if(totalValue > 0) newShare = total.mul(lusdAmount) / totalValue; + + // deposit + require(LUSD.transferFrom(msg.sender, address(this), lusdAmount), "deposit: transferFrom failed"); + SP.provideToSP(lusdAmount, frontEndTag); + + // update LQTY + join(msg.sender, newShare); + } + + function withdraw(uint numShares) external { + uint lusdValue = SP.getCompoundedLUSDDeposit(address(this)); + uint ethValue = SP.getDepositorETHGain(address(this)).add(address(this).balance); + + uint lusdAmount = lusdValue.mul(numShares).div(total); + uint ethAmount = ethValue.mul(numShares).div(total); + + // this withdraws lusd, lqty, and eth + SP.withdrawFromSP(lusdAmount); + + // update LQTY + exit(msg.sender, numShares); + + // send lusd and eth + if(lusdAmount > 0) LUSD.transfer(msg.sender, lusdAmount); + if(ethAmount > 0) { + (bool success, ) = msg.sender.call{ value: ethAmount }(""); // re-entry is fine here + require(success, "withdraw: sending ETH failed"); + } + } + + function addBps(uint n, int bps) internal pure returns(uint) { + require(bps <= 10000, "reduceBps: bps exceeds max"); + require(bps >= -10000, "reduceBps: bps exceeds min"); + + return n.mul(uint(10000 + bps)) / 10000; + } + + function getSwapEthAmount(uint lusdQty) public view returns(uint ethAmount, uint feeEthAmount) { + uint lusdBalance = SP.getCompoundedLUSDDeposit(address(this)); + uint ethBalance = SP.getDepositorETHGain(address(this)).add(address(this).balance); + + uint eth2usdPrice = fetchPrice(); + if(eth2usdPrice == 0) return (0, 0); // chainlink is down + + uint ethUsdValue = ethBalance.mul(eth2usdPrice) / PRECISION; + uint maxReturn = addBps(lusdQty.mul(PRECISION) / eth2usdPrice, int(maxDiscount)); + + uint xQty = lusdQty; + uint xBalance = lusdBalance; + uint yBalance = lusdBalance.add(ethUsdValue.mul(2)); + + uint usdReturn = getReturn(xQty, xBalance, yBalance, A); + uint basicEthReturn = usdReturn.mul(PRECISION) / eth2usdPrice; + + if(ethBalance < basicEthReturn) basicEthReturn = ethBalance; // cannot give more than balance + if(maxReturn < basicEthReturn) basicEthReturn = maxReturn; + + ethAmount = addBps(basicEthReturn, -int(fee)); + feeEthAmount = basicEthReturn.sub(ethAmount); + } + + // get ETH in return to LUSD + function swap(uint lusdAmount, address payable dest) public payable returns(uint) { + (uint ethAmount, uint feeAmount) = getSwapEthAmount(lusdAmount); + LUSD.transferFrom(msg.sender, address(this), lusdAmount); + SP.provideToSP(lusdAmount, frontEndTag); // TODO - real front end + + if(feeAmount > 0) feePool.transfer(feeAmount); + (bool success, ) = dest.call{ value: ethAmount }(""); // re-entry is fine here + require(success, "swap: sending ETH failed"); + + return ethAmount; + } + + // kyber network reserve compatible function + function trade( + IERC20 srcToken, + uint256 srcAmount, + IERC20 destToken, + address payable destAddress, + uint256 conversionRate, + bool validate + ) external payable returns (bool) { + return swap(srcAmount, destAddress) > 0; + } + + function getConversionRate( + IERC20 src, + IERC20 dest, + uint256 srcQty, + uint256 blockNumber + ) external view returns (uint256) { + (uint ethQty, ) = getSwapEthAmount(srcQty); + return ethQty.mul(PRECISION) / srcQty; + } + + receive() external payable {} +} + +contract Dummy { + fallback() external payable {} +} + +contract DummyGem is Dummy { + function transfer(address, uint) external pure returns(bool) { + return true; + } + + function transferFrom(address, address, uint) external pure returns(bool) { + return true; + } + + function decimals() external pure returns(uint) { + return 18; + } +} \ No newline at end of file diff --git a/packages/contracts/contracts/B.Protocol/ChainlinkTestnet.sol b/packages/contracts/contracts/B.Protocol/ChainlinkTestnet.sol new file mode 100644 index 000000000..d0edb219c --- /dev/null +++ b/packages/contracts/contracts/B.Protocol/ChainlinkTestnet.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.11; + +import "./../TestContracts/PriceFeedTestnet.sol"; + +/* +* PriceFeed placeholder for testnet and development. The price is simply set manually and saved in a state +* variable. The contract does not connect to a live Chainlink price feed. +*/ +contract ChainlinkTestnet { + + PriceFeedTestnet feed; + + constructor(PriceFeedTestnet _feed) public { + feed = _feed; + } + + function decimals() external pure returns(uint) { + return 18; + } + + function latestRoundData() external view returns + ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 timestamp, + uint80 answeredInRound + ) + { + answer = int(feed.getPrice()); + timestamp = now; + } +} diff --git a/packages/contracts/contracts/B.Protocol/PriceFormula.sol b/packages/contracts/contracts/B.Protocol/PriceFormula.sol new file mode 100644 index 000000000..b685ad041 --- /dev/null +++ b/packages/contracts/contracts/B.Protocol/PriceFormula.sol @@ -0,0 +1,50 @@ +pragma solidity 0.6.11; + +import "./../Dependencies/SafeMath.sol"; + +contract PriceFormula { + using SafeMath for uint256; + + function getSumFixedPoint(uint x, uint y, uint A) public pure returns(uint) { + if(x == 0 && y == 0) return 0; + + uint sum = x.add(y); + + for(uint i = 0 ; i < 255 ; i++) { + uint dP = sum; + dP = dP.mul(sum) / (x.mul(2)).add(1); + dP = dP.mul(sum) / (y.mul(2)).add(1); + + uint prevSum = sum; + + uint n = (A.mul(2).mul(x.add(y)).add(dP.mul(2))).mul(sum); + uint d = (A.mul(2).sub(1).mul(sum)); + sum = n / d.add(dP.mul(3)); + + if(sum <= prevSum.add(1) && prevSum.add(1) <= sum) break; + } + + return sum; + } + + function getReturn(uint xQty, uint xBalance, uint yBalance, uint A) public pure returns(uint) { + uint sum = getSumFixedPoint(xBalance, yBalance, A); + + uint c = sum.mul(sum) / (xQty.add(xBalance)).mul(2); + c = c.mul(sum) / A.mul(4); + uint b = (xQty.add(xBalance)).add(sum / A.mul(2)); + uint yPrev = 0; + uint y = sum; + + for(uint i = 0 ; i < 255 ; i++) { + yPrev = y; + uint n = (y.mul(y)).add(c); + uint d = y.mul(2).add(b).sub(sum); + y = n / d; + + if(y <= yPrev.add(1) && yPrev.add(1) <= y) break; + } + + return yBalance.sub(y).sub(1); + } +} diff --git a/packages/contracts/contracts/B.Protocol/crop.sol b/packages/contracts/contracts/B.Protocol/crop.sol new file mode 100644 index 000000000..7d43ec40e --- /dev/null +++ b/packages/contracts/contracts/B.Protocol/crop.sol @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2021 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity 0.6.11; + +interface VatLike { + function urns(bytes32, address) external view returns (uint256, uint256); + function gem(bytes32, address) external view returns (uint256); + function slip(bytes32, address, int256) external; +} + +interface ERC20 { + function balanceOf(address owner) external view returns (uint256); + function transfer(address dst, uint256 amount) external returns (bool); + function transferFrom(address src, address dst, uint256 amount) external returns (bool); + function approve(address spender, uint256 amount) external returns (bool); + function allowance(address owner, address spender) external view returns (uint256); + function decimals() external returns (uint8); +} + +// receives tokens and shares them among holders +contract CropJoin { + + VatLike public immutable vat; // cdp engine + bytes32 public immutable ilk; // collateral type + ERC20 public immutable gem; // collateral token + uint256 public immutable dec; // gem decimals + ERC20 public immutable bonus; // rewards token + + uint256 public share; // crops per gem [ray] + uint256 public total; // total gems [wad] + uint256 public stock; // crop balance [wad] + + mapping (address => uint256) public crops; // crops per user [wad] + mapping (address => uint256) public stake; // gems per user [wad] + + uint256 immutable internal to18ConversionFactor; + uint256 immutable internal toGemConversionFactor; + + // --- Events --- + event Join(uint256 val); + event Exit(uint256 val); + event Flee(); + event Tack(address indexed src, address indexed dst, uint256 wad); + + constructor(address vat_, bytes32 ilk_, address gem_, address bonus_) public { + vat = VatLike(vat_); + ilk = ilk_; + gem = ERC20(gem_); + uint256 dec_ = ERC20(gem_).decimals(); + require(dec_ <= 18); + dec = dec_; + to18ConversionFactor = 10 ** (18 - dec_); + toGemConversionFactor = 10 ** dec_; + + bonus = ERC20(bonus_); + } + + function add(uint256 x, uint256 y) public pure returns (uint256 z) { + require((z = x + y) >= x, "ds-math-add-overflow"); + } + function sub(uint256 x, uint256 y) public pure returns (uint256 z) { + require((z = x - y) <= x, "ds-math-sub-underflow"); + } + function mul(uint256 x, uint256 y) public pure returns (uint256 z) { + require(y == 0 || (z = x * y) / y == x, "ds-math-mul-overflow"); + } + function divup(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = add(x, sub(y, 1)) / y; + } + uint256 constant WAD = 10 ** 18; + function wmul(uint256 x, uint256 y) public pure returns (uint256 z) { + z = mul(x, y) / WAD; + } + function wdiv(uint256 x, uint256 y) public pure returns (uint256 z) { + z = mul(x, WAD) / y; + } + function wdivup(uint256 x, uint256 y) public pure returns (uint256 z) { + z = divup(mul(x, WAD), y); + } + uint256 constant RAY = 10 ** 27; + function rmul(uint256 x, uint256 y) public pure returns (uint256 z) { + z = mul(x, y) / RAY; + } + function rmulup(uint256 x, uint256 y) public pure returns (uint256 z) { + z = divup(mul(x, y), RAY); + } + function rdiv(uint256 x, uint256 y) public pure returns (uint256 z) { + z = mul(x, RAY) / y; + } + + // Net Asset Valuation [wad] + function nav() public virtual returns (uint256) { + return total; + } + + // Net Assets per Share [wad] + function nps() public returns (uint256) { + if (total == 0) return WAD; + else return wdiv(nav(), total); + } + + function crop() internal virtual returns (uint256) { + return sub(bonus.balanceOf(address(this)), stock); + } + + function harvest(address from, address to) internal { + if (total > 0) share = add(share, rdiv(crop(), total)); + + uint256 last = crops[from]; + uint256 curr = rmul(stake[from], share); + if (curr > last) require(bonus.transfer(to, curr - last)); + stock = bonus.balanceOf(address(this)); + } + + function join(address urn, uint256 val) internal virtual { + harvest(urn, urn); + if (val > 0) { + uint256 wad = wdiv(mul(val, to18ConversionFactor), nps()); + + // Overflow check for int256(wad) cast below + // Also enforces a non-zero wad + require(int256(wad) > 0); + + require(gem.transferFrom(msg.sender, address(this), val)); + vat.slip(ilk, urn, int256(wad)); + + total = add(total, wad); + stake[urn] = add(stake[urn], wad); + } + crops[urn] = rmulup(stake[urn], share); + emit Join(val); + } + + function exit(address guy, uint256 val) internal virtual { + harvest(msg.sender, guy); + if (val > 0) { + uint256 wad = wdivup(mul(val, to18ConversionFactor), nps()); + + // Overflow check for int256(wad) cast below + // Also enforces a non-zero wad + require(int256(wad) > 0); + + require(gem.transfer(guy, val)); + vat.slip(ilk, msg.sender, -int256(wad)); + + total = sub(total, wad); + stake[msg.sender] = sub(stake[msg.sender], wad); + } + crops[msg.sender] = rmulup(stake[msg.sender], share); + emit Exit(val); + } + + function flee() internal virtual { + uint256 wad = vat.gem(ilk, msg.sender); + require(wad <= 2 ** 255); + uint256 val = wmul(wmul(wad, nps()), toGemConversionFactor); + + require(gem.transfer(msg.sender, val)); + vat.slip(ilk, msg.sender, -int256(wad)); + + total = sub(total, wad); + stake[msg.sender] = sub(stake[msg.sender], wad); + crops[msg.sender] = rmulup(stake[msg.sender], share); + + emit Flee(); + } +} diff --git a/packages/contracts/test/B.Protocol/BAMMTest.js b/packages/contracts/test/B.Protocol/BAMMTest.js new file mode 100644 index 000000000..e7bdcf749 --- /dev/null +++ b/packages/contracts/test/B.Protocol/BAMMTest.js @@ -0,0 +1,3869 @@ +const deploymentHelper = require("./../../utils/deploymentHelpers.js") +const testHelpers = require("./../../utils/testHelpers.js") +const th = testHelpers.TestHelper +const dec = th.dec +const toBN = th.toBN +const mv = testHelpers.MoneyValues +const timeValues = testHelpers.TimeValues + +const TroveManagerTester = artifacts.require("TroveManagerTester") +const LUSDToken = artifacts.require("LUSDToken") +const NonPayable = artifacts.require('NonPayable.sol') +const BAMM = artifacts.require("BAMM.sol") +const ChainlinkTestnet = artifacts.require("ChainlinkTestnet.sol") + +const ZERO = toBN('0') +const ZERO_ADDRESS = th.ZERO_ADDRESS +const maxBytes32 = th.maxBytes32 + +const getFrontEndTag = async (stabilityPool, depositor) => { + return (await stabilityPool.deposits(depositor))[1] +} + +contract('StabilityPool', async accounts => { + const [owner, + defaulter_1, defaulter_2, defaulter_3, + whale, + alice, bob, carol, dennis, erin, flyn, + A, B, C, D, E, F, + frontEnd_1, frontEnd_2, frontEnd_3, + ] = accounts; + + const [bountyAddress, lpRewardsAddress, multisig] = accounts.slice(997, 1000) + + const frontEnds = [frontEnd_1, frontEnd_2, frontEnd_3] + let contracts + let priceFeed + let lusdToken + let sortedTroves + let troveManager + let activePool + let stabilityPool + let bamm + let chainlink + let defaultPool + let borrowerOperations + let lqtyToken + let communityIssuance + + let gasPriceInWei + + const feePool = "0x1000000000000000000000000000000000000001" + + const getOpenTroveLUSDAmount = async (totalDebt) => th.getOpenTroveLUSDAmount(contracts, totalDebt) + const openTrove = async (params) => th.openTrove(contracts, params) + const assertRevert = th.assertRevert + + describe("Stability Pool Mechanisms", async () => { + + before(async () => { + gasPriceInWei = await web3.eth.getGasPrice() + }) + + beforeEach(async () => { + contracts = await deploymentHelper.deployLiquityCore() + contracts.troveManager = await TroveManagerTester.new() + contracts.lusdToken = await LUSDToken.new( + contracts.troveManager.address, + contracts.stabilityPool.address, + contracts.borrowerOperations.address + ) + const LQTYContracts = await deploymentHelper.deployLQTYContracts(bountyAddress, lpRewardsAddress, multisig) + + priceFeed = contracts.priceFeedTestnet + lusdToken = contracts.lusdToken + sortedTroves = contracts.sortedTroves + troveManager = contracts.troveManager + activePool = contracts.activePool + stabilityPool = contracts.stabilityPool + defaultPool = contracts.defaultPool + borrowerOperations = contracts.borrowerOperations + hintHelpers = contracts.hintHelpers + + lqtyToken = LQTYContracts.lqtyToken + communityIssuance = LQTYContracts.communityIssuance + + await deploymentHelper.connectLQTYContracts(LQTYContracts) + await deploymentHelper.connectCoreContracts(contracts, LQTYContracts) + await deploymentHelper.connectLQTYContractsToCore(LQTYContracts, contracts) + + // Register 3 front ends + await th.registerFrontEnds(frontEnds, stabilityPool) + + // deploy BAMM + chainlink = await ChainlinkTestnet.new(priceFeed.address) + bamm = await BAMM.new(chainlink.address, stabilityPool.address, lusdToken.address, lqtyToken.address, 400, feePool) + }) + + // --- provideToSP() --- + // increases recorded LUSD at Stability Pool + it.only("deposit(): increases the Stability Pool LUSD balance", async () => { + // --- SETUP --- Give Alice a least 200 + await openTrove({ extraLUSDAmount: toBN(200), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + + // --- TEST --- + await lusdToken.approve(bamm.address, toBN(200), { from: alice }) + await bamm.deposit(toBN(200), { from: alice }) + + // check LUSD balances after + const stabilityPool_LUSD_After = await stabilityPool.getTotalLUSDDeposits() + assert.equal(stabilityPool_LUSD_After, 200) + }) + + // --- provideToSP() --- + // increases recorded LUSD at Stability Pool + it.only("deposit(): two users deposit, check their share", async () => { + // --- SETUP --- Give Alice a least 200 + await openTrove({ extraLUSDAmount: toBN(200), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await openTrove({ extraLUSDAmount: toBN(200), ICR: toBN(dec(2, 18)), extraParams: { from: whale } }) + + // --- TEST --- + await lusdToken.approve(bamm.address, toBN(200), { from: alice }) + await lusdToken.approve(bamm.address, toBN(200), { from: whale }) + await bamm.deposit(toBN(200), { from: alice }) + await bamm.deposit(toBN(200), { from: whale }) + + // check LUSD balances after1 + const whaleShare = await bamm.stake(whale) + const aliceShare = await bamm.stake(alice) + + assert.equal(whaleShare.toString(), aliceShare.toString()) + }) + + // --- provideToSP() --- + // increases recorded LUSD at Stability Pool + it.only("deposit(): two users deposit, one withdraw. check their share", async () => { + // --- SETUP --- Give Alice a least 200 + await openTrove({ extraLUSDAmount: toBN(200), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await openTrove({ extraLUSDAmount: toBN(200), ICR: toBN(dec(2, 18)), extraParams: { from: whale } }) + + // --- TEST --- + await lusdToken.approve(bamm.address, toBN(200), { from: alice }) + await lusdToken.approve(bamm.address, toBN(100), { from: whale }) + await bamm.deposit(toBN(200), { from: alice }) + await bamm.deposit(toBN(100), { from: whale }) + + // check LUSD balances after1 + const whaleShare = await bamm.stake(whale) + const aliceShare = await bamm.stake(alice) + + assert.equal(whaleShare.mul(toBN(2)).toString(), aliceShare.toString()) + + const whaleBalanceBefore = await lusdToken.balanceOf(whale) + const shareToWithdraw = whaleShare.div(toBN(2)); + await bamm.withdraw(shareToWithdraw, { from: whale }); + + const newWhaleShare = await bamm.stake(whale) + assert.equal(newWhaleShare.mul(toBN(2)).toString(), whaleShare.toString()) + + const whaleBalanceAfter = await lusdToken.balanceOf(whale) + assert.equal(whaleBalanceAfter.sub(whaleBalanceBefore).toString(), 50) + }) + + it.only('rebalance scenario', async () => { + // --- SETUP --- + + // Whale opens Trove and deposits to SP + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + const whaleLUSD = await lusdToken.balanceOf(whale) + await lusdToken.approve(bamm.address, whaleLUSD, { from: whale }) + bamm.deposit(whaleLUSD, { from: whale } ) + + // 2 Troves opened, each withdraws minimum debt + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1, } }) + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2, } }) + + // Alice makes Trove and withdraws 100 LUSD + await openTrove({ extraLUSDAmount: toBN(dec(100, 18)), ICR: toBN(dec(5, 18)), extraParams: { from: alice, value: dec(50, 'ether') } }) + + + // price drops: defaulter's Troves fall below MCR, whale doesn't + await priceFeed.setPrice(dec(105, 18)); + console.log("rebalance", (await bamm.fetchPrice()).toString()) + + const SPLUSD_Before = await stabilityPool.getTotalLUSDDeposits() + + // Troves are closed + await troveManager.liquidate(defaulter_1, { from: owner }) + await troveManager.liquidate(defaulter_2, { from: owner }) + + // Confirm SP has decreased + const SPLUSD_After = await stabilityPool.getTotalLUSDDeposits() + assert.isTrue(SPLUSD_After.lt(SPLUSD_Before)) + + console.log((await stabilityPool.getCompoundedLUSDDeposit(bamm.address)).toString()) + console.log((await stabilityPool.getDepositorETHGain(bamm.address)).toString()) + const price = await priceFeed.fetchPrice.call() + console.log(price.toString()) + + const ammExpectedEth = await bamm.getSwapEthAmount.call(toBN(dec(1, 18))) + + console.log("expected eth amount", ammExpectedEth.ethAmount.toString()) + + const rate = await bamm.getConversionRate(lusdToken.address, "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", toBN(dec(1, 18)), 0) + assert.equal(rate.toString(), ammExpectedEth.ethAmount.toString()) + + await lusdToken.approve(bamm.address, toBN(dec(1, 18)), { from: alice }) + + const dest = "0xe1A587Ac322da1611DF55b11A6bC8c6052D896cE" // dummy address + //await bamm.swap(toBN(dec(1, 18)), dest, { from: alice }) + await bamm.trade(lusdToken.address, toBN(dec(1, 18)), "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", dest, rate, true, { from: alice }); + + const swapBalance = await web3.eth.getBalance(dest) + + assert.equal(swapBalance, ammExpectedEth.ethAmount) + }) + + it.only("test basic LQTY allocation", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C, open troves + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: E } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: F } }) + + // D, E provide to bamm, F provide to SP + await lusdToken.approve(bamm.address, dec(1000, 18), { from: D }) + await lusdToken.approve(bamm.address, dec(2000, 18), { from: E }) + await bamm.deposit(dec(1000, 18), { from: D }) + await bamm.deposit(dec(2000, 18), { from: E }) + await stabilityPool.provideToSP(dec(3000, 18), "0x0000000000000000000000000000000000000000", { from: F }) + + // Get F1, F2, F3 LQTY balances before, and confirm they're zero + const D_LQTYBalance_Before = await lqtyToken.balanceOf(D) + const E_LQTYBalance_Before = await lqtyToken.balanceOf(E) + const F_LQTYBalance_Before = await lqtyToken.balanceOf(F) + + assert.equal(D_LQTYBalance_Before, '0') + assert.equal(E_LQTYBalance_Before, '0') + assert.equal(F_LQTYBalance_Before, '0') + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + await stabilityPool.withdrawFromSP(0, { from: F }) + await bamm.withdraw(0, { from: D }) + await bamm.withdraw(0, { from: E }) + + // Get F1, F2, F3 LQTY balances after, and confirm they have increased + const D_LQTYBalance_After = await lqtyToken.balanceOf(D) + const E_LQTYBalance_After = await lqtyToken.balanceOf(E) + const F_LQTYBalance_After = await lqtyToken.balanceOf(F) + + assert.equal(D_LQTYBalance_After.add(E_LQTYBalance_After).toString(), F_LQTYBalance_After.toString()) + }) + + it.only("test complex LQTY allocation", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C, open troves + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: E } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: F } }) + + const A_LQTYBalance_Before = await lqtyToken.balanceOf(A) + const D_LQTYBalance_Before = await lqtyToken.balanceOf(D) + const E_LQTYBalance_Before = await lqtyToken.balanceOf(E) + const F_LQTYBalance_Before = await lqtyToken.balanceOf(F) + + assert.equal(A_LQTYBalance_Before, '0') + assert.equal(D_LQTYBalance_Before, '0') + assert.equal(E_LQTYBalance_Before, '0') + assert.equal(F_LQTYBalance_Before, '0') + + // D, E provide to bamm, F provide to SP + await lusdToken.approve(bamm.address, dec(1000, 18), { from: D }) + await lusdToken.approve(bamm.address, dec(2000, 18), { from: E }) + await lusdToken.approve(bamm.address, dec(3000, 18), { from: F }) + + await bamm.deposit(dec(1000, 18), { from: D }) + await bamm.deposit(dec(2000, 18), { from: E }) + //await bamm.deposit(dec(3000, 18), { from: F }) + + await bamm.withdraw(0, { from: D }) + console.log((await lqtyToken.balanceOf(D)).toString()) + + console.log("share:", (await bamm.share.call()).toString()) + console.log("stake D:", (await bamm.stake(D)).toString()) + console.log("stake E:", (await bamm.stake(E)).toString()) + + await stabilityPool.provideToSP(dec(1000, 18), "0x0000000000000000000000000000000000000000", { from: A }) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + await bamm.deposit(dec(3000, 18), { from: F }) + await stabilityPool.provideToSP(dec(3000, 18), "0x0000000000000000000000000000000000000000", { from: B }) + + await stabilityPool.withdrawFromSP(0, { from: A }) + console.log("lqty A", (await lqtyToken.balanceOf(A)).toString()) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + console.log("share:", (await bamm.share()).toString()) + console.log("stake D:", (await bamm.stake(D)).toString()) + console.log("stake E:", (await bamm.stake(E)).toString()) + console.log("stake F:", (await bamm.stake(F)).toString()) + + await stabilityPool.withdrawFromSP(0, { from: A }) + console.log("lqty A", (await lqtyToken.balanceOf(A)).toString()) + + await stabilityPool.withdrawFromSP(0, { from: A }) + await stabilityPool.withdrawFromSP(0, { from: B }) + await bamm.withdraw(0, { from: D }) + await bamm.withdraw(0, { from: E }) + await bamm.withdraw(0, { from: F }) + + console.log("lqty D", (await lqtyToken.balanceOf(D)).toString()) + console.log("lqty E", (await lqtyToken.balanceOf(E)).toString()) + console.log("lqty F", (await lqtyToken.balanceOf(F)).toString()) + + console.log("share:", (await bamm.share()).toString()) + console.log("stake D:", (await bamm.stake(D)).toString()) + console.log("stake E:", (await bamm.stake(E)).toString()) + console.log("stake F:", (await bamm.stake(F)).toString()) + + // Get F1, F2, F3 LQTY balances after, and confirm they have increased + const A_LQTYBalance_After = await lqtyToken.balanceOf(A) + const B_LQTYBalance_After = await lqtyToken.balanceOf(B) + const D_LQTYBalance_After = await lqtyToken.balanceOf(D) + const E_LQTYBalance_After = await lqtyToken.balanceOf(E) + const F_LQTYBalance_After = await lqtyToken.balanceOf(F) + + assert.equal(D_LQTYBalance_After.toString(), A_LQTYBalance_After.toString()) + assert.equal(E_LQTYBalance_After.toString(), A_LQTYBalance_After.mul(toBN(2)).toString()) + assert.equal(F_LQTYBalance_After.toString(), B_LQTYBalance_After.toString()) + }) + + it("provideToSP(): reverts if user tries to provide more than their LUSD balance", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice, value: dec(50, 'ether') } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob, value: dec(50, 'ether') } }) + const aliceLUSDbal = await lusdToken.balanceOf(alice) + const bobLUSDbal = await lusdToken.balanceOf(bob) + + // Alice, attempts to deposit 1 wei more than her balance + + const aliceTxPromise = stabilityPool.provideToSP(aliceLUSDbal.add(toBN(1)), frontEnd_1, { from: alice }) + await assertRevert(aliceTxPromise, "revert") + + // Bob, attempts to deposit 235534 more than his balance + + const bobTxPromise = stabilityPool.provideToSP(bobLUSDbal.add(toBN(dec(235534, 18))), frontEnd_1, { from: bob }) + await assertRevert(bobTxPromise, "revert") + }) + + it("provideToSP(): reverts if user tries to provide 2^256-1 LUSD, which exceeds their balance", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice, value: dec(50, 'ether') } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob, value: dec(50, 'ether') } }) + + const maxBytes32 = web3.utils.toBN("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") + + // Alice attempts to deposit 2^256-1 LUSD + try { + aliceTx = await stabilityPool.provideToSP(maxBytes32, frontEnd_1, { from: alice }) + assert.isFalse(tx.receipt.status) + } catch (error) { + assert.include(error.message, "revert") + } + }) + + it("provideToSP(): reverts if cannot receive ETH Gain", async () => { + // --- SETUP --- + // Whale deposits 1850 LUSD in StabilityPool + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + await stabilityPool.provideToSP(dec(1850, 18), frontEnd_1, { from: whale }) + + // Defaulter Troves opened + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) + + // --- TEST --- + + const nonPayable = await NonPayable.new() + await lusdToken.transfer(nonPayable.address, dec(250, 18), { from: whale }) + + // NonPayable makes deposit #1: 150 LUSD + const txData1 = th.getTransactionData('provideToSP(uint256,address)', [web3.utils.toHex(dec(150, 18)), frontEnd_1]) + const tx1 = await nonPayable.forward(stabilityPool.address, txData1) + + const gain_0 = await stabilityPool.getDepositorETHGain(nonPayable.address) + assert.isTrue(gain_0.eq(toBN(0)), 'NonPayable should not have accumulated gains') + + // price drops: defaulters' Troves fall below MCR, nonPayable and whale Trove remain active + await priceFeed.setPrice(dec(105, 18)); + + // 2 defaulters are closed + await troveManager.liquidate(defaulter_1, { from: owner }) + await troveManager.liquidate(defaulter_2, { from: owner }) + + const gain_1 = await stabilityPool.getDepositorETHGain(nonPayable.address) + assert.isTrue(gain_1.gt(toBN(0)), 'NonPayable should have some accumulated gains') + + // NonPayable tries to make deposit #2: 100LUSD (which also attempts to withdraw ETH gain) + const txData2 = th.getTransactionData('provideToSP(uint256,address)', [web3.utils.toHex(dec(100, 18)), frontEnd_1]) + await th.assertRevert(nonPayable.forward(stabilityPool.address, txData2), 'StabilityPool: sending ETH failed') + }) + + it("provideToSP(): doesn't impact other users' deposits or ETH gains", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + + // A, B, C open troves and make Stability Pool deposits + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) + + await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: alice }) + await stabilityPool.provideToSP(dec(2000, 18), frontEnd_1, { from: bob }) + await stabilityPool.provideToSP(dec(3000, 18), frontEnd_1, { from: carol }) + + // D opens a trove + await openTrove({ extraLUSDAmount: toBN(dec(300, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: dennis } }) + + // Would-be defaulters open troves + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) + + // Price drops + await priceFeed.setPrice(dec(105, 18)) + + // Defaulters are liquidated + await troveManager.liquidate(defaulter_1) + await troveManager.liquidate(defaulter_2) + assert.isFalse(await sortedTroves.contains(defaulter_1)) + assert.isFalse(await sortedTroves.contains(defaulter_2)) + + const alice_LUSDDeposit_Before = (await stabilityPool.getCompoundedLUSDDeposit(alice)).toString() + const bob_LUSDDeposit_Before = (await stabilityPool.getCompoundedLUSDDeposit(bob)).toString() + const carol_LUSDDeposit_Before = (await stabilityPool.getCompoundedLUSDDeposit(carol)).toString() + + const alice_ETHGain_Before = (await stabilityPool.getDepositorETHGain(alice)).toString() + const bob_ETHGain_Before = (await stabilityPool.getDepositorETHGain(bob)).toString() + const carol_ETHGain_Before = (await stabilityPool.getDepositorETHGain(carol)).toString() + + //check non-zero LUSD and ETHGain in the Stability Pool + const LUSDinSP = await stabilityPool.getTotalLUSDDeposits() + const ETHinSP = await stabilityPool.getETH() + assert.isTrue(LUSDinSP.gt(mv._zeroBN)) + assert.isTrue(ETHinSP.gt(mv._zeroBN)) + + // D makes an SP deposit + await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: dennis }) + assert.equal((await stabilityPool.getCompoundedLUSDDeposit(dennis)).toString(), dec(1000, 18)) + + const alice_LUSDDeposit_After = (await stabilityPool.getCompoundedLUSDDeposit(alice)).toString() + const bob_LUSDDeposit_After = (await stabilityPool.getCompoundedLUSDDeposit(bob)).toString() + const carol_LUSDDeposit_After = (await stabilityPool.getCompoundedLUSDDeposit(carol)).toString() + + const alice_ETHGain_After = (await stabilityPool.getDepositorETHGain(alice)).toString() + const bob_ETHGain_After = (await stabilityPool.getDepositorETHGain(bob)).toString() + const carol_ETHGain_After = (await stabilityPool.getDepositorETHGain(carol)).toString() + + // Check compounded deposits and ETH gains for A, B and C have not changed + assert.equal(alice_LUSDDeposit_Before, alice_LUSDDeposit_After) + assert.equal(bob_LUSDDeposit_Before, bob_LUSDDeposit_After) + assert.equal(carol_LUSDDeposit_Before, carol_LUSDDeposit_After) + + assert.equal(alice_ETHGain_Before, alice_ETHGain_After) + assert.equal(bob_ETHGain_Before, bob_ETHGain_After) + assert.equal(carol_ETHGain_Before, carol_ETHGain_After) + }) + + it("provideToSP(): doesn't impact system debt, collateral or TCR", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + + // A, B, C open troves and make Stability Pool deposits + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) + + await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: alice }) + await stabilityPool.provideToSP(dec(2000, 18), frontEnd_1, { from: bob }) + await stabilityPool.provideToSP(dec(3000, 18), frontEnd_1, { from: carol }) + + // D opens a trove + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: dennis } }) + + // Would-be defaulters open troves + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) + + // Price drops + await priceFeed.setPrice(dec(105, 18)) + + // Defaulters are liquidated + await troveManager.liquidate(defaulter_1) + await troveManager.liquidate(defaulter_2) + assert.isFalse(await sortedTroves.contains(defaulter_1)) + assert.isFalse(await sortedTroves.contains(defaulter_2)) + + const activeDebt_Before = (await activePool.getLUSDDebt()).toString() + const defaultedDebt_Before = (await defaultPool.getLUSDDebt()).toString() + const activeColl_Before = (await activePool.getETH()).toString() + const defaultedColl_Before = (await defaultPool.getETH()).toString() + const TCR_Before = (await th.getTCR(contracts)).toString() + + // D makes an SP deposit + await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: dennis }) + assert.equal((await stabilityPool.getCompoundedLUSDDeposit(dennis)).toString(), dec(1000, 18)) + + const activeDebt_After = (await activePool.getLUSDDebt()).toString() + const defaultedDebt_After = (await defaultPool.getLUSDDebt()).toString() + const activeColl_After = (await activePool.getETH()).toString() + const defaultedColl_After = (await defaultPool.getETH()).toString() + const TCR_After = (await th.getTCR(contracts)).toString() + + // Check total system debt, collateral and TCR have not changed after a Stability deposit is made + assert.equal(activeDebt_Before, activeDebt_After) + assert.equal(defaultedDebt_Before, defaultedDebt_After) + assert.equal(activeColl_Before, activeColl_After) + assert.equal(defaultedColl_Before, defaultedColl_After) + assert.equal(TCR_Before, TCR_After) + }) + + it("provideToSP(): doesn't impact any troves, including the caller's trove", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + + // A, B, C open troves and make Stability Pool deposits + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) + + // A and B provide to SP + await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: alice }) + await stabilityPool.provideToSP(dec(2000, 18), frontEnd_1, { from: bob }) + + // D opens a trove + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: dennis } }) + + // Price drops + await priceFeed.setPrice(dec(105, 18)) + const price = await priceFeed.getPrice() + + // Get debt, collateral and ICR of all existing troves + const whale_Debt_Before = (await troveManager.Troves(whale))[0].toString() + const alice_Debt_Before = (await troveManager.Troves(alice))[0].toString() + const bob_Debt_Before = (await troveManager.Troves(bob))[0].toString() + const carol_Debt_Before = (await troveManager.Troves(carol))[0].toString() + const dennis_Debt_Before = (await troveManager.Troves(dennis))[0].toString() + + const whale_Coll_Before = (await troveManager.Troves(whale))[1].toString() + const alice_Coll_Before = (await troveManager.Troves(alice))[1].toString() + const bob_Coll_Before = (await troveManager.Troves(bob))[1].toString() + const carol_Coll_Before = (await troveManager.Troves(carol))[1].toString() + const dennis_Coll_Before = (await troveManager.Troves(dennis))[1].toString() + + const whale_ICR_Before = (await troveManager.getCurrentICR(whale, price)).toString() + const alice_ICR_Before = (await troveManager.getCurrentICR(alice, price)).toString() + const bob_ICR_Before = (await troveManager.getCurrentICR(bob, price)).toString() + const carol_ICR_Before = (await troveManager.getCurrentICR(carol, price)).toString() + const dennis_ICR_Before = (await troveManager.getCurrentICR(dennis, price)).toString() + + // D makes an SP deposit + await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: dennis }) + assert.equal((await stabilityPool.getCompoundedLUSDDeposit(dennis)).toString(), dec(1000, 18)) + + const whale_Debt_After = (await troveManager.Troves(whale))[0].toString() + const alice_Debt_After = (await troveManager.Troves(alice))[0].toString() + const bob_Debt_After = (await troveManager.Troves(bob))[0].toString() + const carol_Debt_After = (await troveManager.Troves(carol))[0].toString() + const dennis_Debt_After = (await troveManager.Troves(dennis))[0].toString() + + const whale_Coll_After = (await troveManager.Troves(whale))[1].toString() + const alice_Coll_After = (await troveManager.Troves(alice))[1].toString() + const bob_Coll_After = (await troveManager.Troves(bob))[1].toString() + const carol_Coll_After = (await troveManager.Troves(carol))[1].toString() + const dennis_Coll_After = (await troveManager.Troves(dennis))[1].toString() + + const whale_ICR_After = (await troveManager.getCurrentICR(whale, price)).toString() + const alice_ICR_After = (await troveManager.getCurrentICR(alice, price)).toString() + const bob_ICR_After = (await troveManager.getCurrentICR(bob, price)).toString() + const carol_ICR_After = (await troveManager.getCurrentICR(carol, price)).toString() + const dennis_ICR_After = (await troveManager.getCurrentICR(dennis, price)).toString() + + assert.equal(whale_Debt_Before, whale_Debt_After) + assert.equal(alice_Debt_Before, alice_Debt_After) + assert.equal(bob_Debt_Before, bob_Debt_After) + assert.equal(carol_Debt_Before, carol_Debt_After) + assert.equal(dennis_Debt_Before, dennis_Debt_After) + + assert.equal(whale_Coll_Before, whale_Coll_After) + assert.equal(alice_Coll_Before, alice_Coll_After) + assert.equal(bob_Coll_Before, bob_Coll_After) + assert.equal(carol_Coll_Before, carol_Coll_After) + assert.equal(dennis_Coll_Before, dennis_Coll_After) + + assert.equal(whale_ICR_Before, whale_ICR_After) + assert.equal(alice_ICR_Before, alice_ICR_After) + assert.equal(bob_ICR_Before, bob_ICR_After) + assert.equal(carol_ICR_Before, carol_ICR_After) + assert.equal(dennis_ICR_Before, dennis_ICR_After) + }) + + it("provideToSP(): doesn't protect the depositor's trove from liquidation", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + + // A, B, C open troves and make Stability Pool deposits + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) + + // A, B provide 100 LUSD to SP + await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: alice }) + await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: bob }) + + // Confirm Bob has an active trove in the system + assert.isTrue(await sortedTroves.contains(bob)) + assert.equal((await troveManager.getTroveStatus(bob)).toString(), '1') // Confirm Bob's trove status is active + + // Confirm Bob has a Stability deposit + assert.equal((await stabilityPool.getCompoundedLUSDDeposit(bob)).toString(), dec(1000, 18)) + + // Price drops + await priceFeed.setPrice(dec(105, 18)) + const price = await priceFeed.getPrice() + + // Liquidate bob + await troveManager.liquidate(bob) + + // Check Bob's trove has been removed from the system + assert.isFalse(await sortedTroves.contains(bob)) + assert.equal((await troveManager.getTroveStatus(bob)).toString(), '3') // check Bob's trove status was closed by liquidation + }) + + it("provideToSP(): providing 0 LUSD reverts", async () => { + // --- SETUP --- + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + + // A, B, C open troves and make Stability Pool deposits + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) + + // A, B, C provides 100, 50, 30 LUSD to SP + await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: alice }) + await stabilityPool.provideToSP(dec(50, 18), frontEnd_1, { from: bob }) + await stabilityPool.provideToSP(dec(30, 18), frontEnd_1, { from: carol }) + + const bob_Deposit_Before = (await stabilityPool.getCompoundedLUSDDeposit(bob)).toString() + const LUSDinSP_Before = (await stabilityPool.getTotalLUSDDeposits()).toString() + + assert.equal(LUSDinSP_Before, dec(180, 18)) + + // Bob provides 0 LUSD to the Stability Pool + const txPromise_B = stabilityPool.provideToSP(0, frontEnd_1, { from: bob }) + await th.assertRevert(txPromise_B) + }) + + // --- LQTY functionality --- + it("provideToSP(), new deposit: when SP > 0, triggers LQTY reward event - increases the sum G", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + + // A, B, C open troves and make Stability Pool deposits + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + + // A provides to SP + await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: A }) + + let currentEpoch = await stabilityPool.currentEpoch() + let currentScale = await stabilityPool.currentScale() + const G_Before = await stabilityPool.epochToScaleToG(currentEpoch, currentScale) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // B provides to SP + await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: B }) + + currentEpoch = await stabilityPool.currentEpoch() + currentScale = await stabilityPool.currentScale() + const G_After = await stabilityPool.epochToScaleToG(currentEpoch, currentScale) + + // Expect G has increased from the LQTY reward event triggered + assert.isTrue(G_After.gt(G_Before)) + }) + + it("provideToSP(), new deposit: when SP is empty, doesn't update G", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + + // A, B, C open troves and make Stability Pool deposits + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + + // A provides to SP + await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: A }) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // A withdraws + await stabilityPool.withdrawFromSP(dec(1000, 18), { from: A }) + + // Check SP is empty + assert.equal((await stabilityPool.getTotalLUSDDeposits()), '0') + + // Check G is non-zero + let currentEpoch = await stabilityPool.currentEpoch() + let currentScale = await stabilityPool.currentScale() + const G_Before = await stabilityPool.epochToScaleToG(currentEpoch, currentScale) + + assert.isTrue(G_Before.gt(toBN('0'))) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // B provides to SP + await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: B }) + + currentEpoch = await stabilityPool.currentEpoch() + currentScale = await stabilityPool.currentScale() + const G_After = await stabilityPool.epochToScaleToG(currentEpoch, currentScale) + + // Expect G has not changed + assert.isTrue(G_After.eq(G_Before)) + }) + + it("provideToSP(), new deposit: sets the correct front end tag", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + + // A, B, C, D open troves and make Stability Pool deposits + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) + + // Check A, B, C D have no front end tags + const A_tagBefore = await getFrontEndTag(stabilityPool, A) + const B_tagBefore = await getFrontEndTag(stabilityPool, B) + const C_tagBefore = await getFrontEndTag(stabilityPool, C) + const D_tagBefore = await getFrontEndTag(stabilityPool, D) + + assert.equal(A_tagBefore, ZERO_ADDRESS) + assert.equal(B_tagBefore, ZERO_ADDRESS) + assert.equal(C_tagBefore, ZERO_ADDRESS) + assert.equal(D_tagBefore, ZERO_ADDRESS) + + // A, B, C, D provides to SP + await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(2000, 18), frontEnd_2, { from: B }) + await stabilityPool.provideToSP(dec(3000, 18), frontEnd_3, { from: C }) + await stabilityPool.provideToSP(dec(4000, 18), ZERO_ADDRESS, { from: D }) // transacts directly, no front end + + // Check A, B, C D have no front end tags + const A_tagAfter = await getFrontEndTag(stabilityPool, A) + const B_tagAfter = await getFrontEndTag(stabilityPool, B) + const C_tagAfter = await getFrontEndTag(stabilityPool, C) + const D_tagAfter = await getFrontEndTag(stabilityPool, D) + + // Check front end tags are correctly set + assert.equal(A_tagAfter, frontEnd_1) + assert.equal(B_tagAfter, frontEnd_2) + assert.equal(C_tagAfter, frontEnd_3) + assert.equal(D_tagAfter, ZERO_ADDRESS) + }) + + it("provideToSP(), new deposit: depositor does not receive any LQTY rewards", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + + // A, B, open troves + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + + // Get A, B, C LQTY balances before and confirm they're zero + const A_LQTYBalance_Before = await lqtyToken.balanceOf(A) + const B_LQTYBalance_Before = await lqtyToken.balanceOf(B) + + assert.equal(A_LQTYBalance_Before, '0') + assert.equal(B_LQTYBalance_Before, '0') + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // A, B provide to SP + await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(2000, 18), ZERO_ADDRESS, { from: B }) + + // Get A, B, C LQTY balances after, and confirm they're still zero + const A_LQTYBalance_After = await lqtyToken.balanceOf(A) + const B_LQTYBalance_After = await lqtyToken.balanceOf(B) + + assert.equal(A_LQTYBalance_After, '0') + assert.equal(B_LQTYBalance_After, '0') + }) + + it("provideToSP(), new deposit after past full withdrawal: depositor does not receive any LQTY rewards", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C, open troves + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + await openTrove({ extraLUSDAmount: toBN(dec(4000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) + + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // --- SETUP --- + + const initialDeposit_A = await lusdToken.balanceOf(A) + const initialDeposit_B = await lusdToken.balanceOf(B) + // A, B provide to SP + await stabilityPool.provideToSP(initialDeposit_A, frontEnd_1, { from: A }) + await stabilityPool.provideToSP(initialDeposit_B, frontEnd_2, { from: B }) + + // time passes + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // C deposits. A, and B earn LQTY + await stabilityPool.provideToSP(dec(5, 18), ZERO_ADDRESS, { from: C }) + + // Price drops, defaulter is liquidated, A, B and C earn ETH + await priceFeed.setPrice(dec(105, 18)) + assert.isFalse(await th.checkRecoveryMode(contracts)) + + await troveManager.liquidate(defaulter_1) + + // price bounces back to 200 + await priceFeed.setPrice(dec(200, 18)) + + // A and B fully withdraw from the pool + await stabilityPool.withdrawFromSP(initialDeposit_A, { from: A }) + await stabilityPool.withdrawFromSP(initialDeposit_B, { from: B }) + + // --- TEST --- + + // Get A, B, C LQTY balances before and confirm they're non-zero + const A_LQTYBalance_Before = await lqtyToken.balanceOf(A) + const B_LQTYBalance_Before = await lqtyToken.balanceOf(B) + assert.isTrue(A_LQTYBalance_Before.gt(toBN('0'))) + assert.isTrue(B_LQTYBalance_Before.gt(toBN('0'))) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // A, B provide to SP + await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(200, 18), ZERO_ADDRESS, { from: B }) + + // Get A, B, C LQTY balances after, and confirm they have not changed + const A_LQTYBalance_After = await lqtyToken.balanceOf(A) + const B_LQTYBalance_After = await lqtyToken.balanceOf(B) + + assert.isTrue(A_LQTYBalance_After.eq(A_LQTYBalance_Before)) + assert.isTrue(B_LQTYBalance_After.eq(B_LQTYBalance_Before)) + }) + + it("provideToSP(), new eligible deposit: tagged front end receives LQTY rewards", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C, open troves + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: E } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: F } }) + + // D, E, F provide to SP + await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: D }) + await stabilityPool.provideToSP(dec(2000, 18), frontEnd_2, { from: E }) + await stabilityPool.provideToSP(dec(3000, 18), frontEnd_3, { from: F }) + + // Get F1, F2, F3 LQTY balances before, and confirm they're zero + const frontEnd_1_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_1) + const frontEnd_2_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_2) + const frontEnd_3_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_3) + + assert.equal(frontEnd_1_LQTYBalance_Before, '0') + assert.equal(frontEnd_2_LQTYBalance_Before, '0') + assert.equal(frontEnd_3_LQTYBalance_Before, '0') + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // console.log(`LQTYSupplyCap before: ${await communityIssuance.LQTYSupplyCap()}`) + // console.log(`totalLQTYIssued before: ${await communityIssuance.totalLQTYIssued()}`) + // console.log(`LQTY balance of CI before: ${await lqtyToken.balanceOf(communityIssuance.address)}`) + + // A, B, C provide to SP + await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(2000, 18), frontEnd_2, { from: B }) + await stabilityPool.provideToSP(dec(3000, 18), frontEnd_3, { from: C }) + + // console.log(`LQTYSupplyCap after: ${await communityIssuance.LQTYSupplyCap()}`) + // console.log(`totalLQTYIssued after: ${await communityIssuance.totalLQTYIssued()}`) + // console.log(`LQTY balance of CI after: ${await lqtyToken.balanceOf(communityIssuance.address)}`) + + // Get F1, F2, F3 LQTY balances after, and confirm they have increased + const frontEnd_1_LQTYBalance_After = await lqtyToken.balanceOf(frontEnd_1) + const frontEnd_2_LQTYBalance_After = await lqtyToken.balanceOf(frontEnd_2) + const frontEnd_3_LQTYBalance_After = await lqtyToken.balanceOf(frontEnd_3) + + assert.isTrue(frontEnd_1_LQTYBalance_After.gt(frontEnd_1_LQTYBalance_Before)) + assert.isTrue(frontEnd_2_LQTYBalance_After.gt(frontEnd_2_LQTYBalance_Before)) + assert.isTrue(frontEnd_3_LQTYBalance_After.gt(frontEnd_3_LQTYBalance_Before)) + }) + + it("provideToSP(), new eligible deposit: tagged front end's stake increases", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C, open troves + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + + // Get front ends' stakes before + const F1_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_1) + const F2_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_2) + const F3_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_3) + + const deposit_A = dec(1000, 18) + const deposit_B = dec(2000, 18) + const deposit_C = dec(3000, 18) + + // A, B, C provide to SP + await stabilityPool.provideToSP(deposit_A, frontEnd_1, { from: A }) + await stabilityPool.provideToSP(deposit_B, frontEnd_2, { from: B }) + await stabilityPool.provideToSP(deposit_C, frontEnd_3, { from: C }) + + // Get front ends' stakes after + const F1_Stake_After = await stabilityPool.frontEndStakes(frontEnd_1) + const F2_Stake_After = await stabilityPool.frontEndStakes(frontEnd_2) + const F3_Stake_After = await stabilityPool.frontEndStakes(frontEnd_3) + + const F1_Diff = F1_Stake_After.sub(F1_Stake_Before) + const F2_Diff = F2_Stake_After.sub(F2_Stake_Before) + const F3_Diff = F3_Stake_After.sub(F3_Stake_Before) + + // Check front ends' stakes have increased by amount equal to the deposit made through them + assert.equal(F1_Diff, deposit_A) + assert.equal(F2_Diff, deposit_B) + assert.equal(F3_Diff, deposit_C) + }) + + it("provideToSP(), new eligible deposit: tagged front end's snapshots update", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C, open troves + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + + // D opens trove + await openTrove({ extraLUSDAmount: toBN(dec(4000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) + + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // --- SETUP --- + + await stabilityPool.provideToSP(dec(2000, 18), ZERO_ADDRESS, { from: D }) + + // fastforward time then make an SP deposit, to make G > 0 + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + await stabilityPool.provideToSP(dec(2000, 18), ZERO_ADDRESS, { from: D }) + + // Perform a liquidation to make 0 < P < 1, and S > 0 + await priceFeed.setPrice(dec(105, 18)) + assert.isFalse(await th.checkRecoveryMode(contracts)) + + await troveManager.liquidate(defaulter_1) + + const currentEpoch = await stabilityPool.currentEpoch() + const currentScale = await stabilityPool.currentScale() + + const S_Before = await stabilityPool.epochToScaleToSum(currentEpoch, currentScale) + const P_Before = await stabilityPool.P() + const G_Before = await stabilityPool.epochToScaleToG(currentEpoch, currentScale) + + // Confirm 0 < P < 1 + assert.isTrue(P_Before.gt(toBN('0')) && P_Before.lt(toBN(dec(1, 18)))) + // Confirm S, G are both > 0 + assert.isTrue(S_Before.gt(toBN('0'))) + assert.isTrue(G_Before.gt(toBN('0'))) + + // Get front ends' snapshots before + for (frontEnd of [frontEnd_1, frontEnd_2, frontEnd_3]) { + const snapshot = await stabilityPool.frontEndSnapshots(frontEnd) + + assert.equal(snapshot[0], '0') // S (should always be 0 for front ends, since S corresponds to ETH gain) + assert.equal(snapshot[1], '0') // P + assert.equal(snapshot[2], '0') // G + assert.equal(snapshot[3], '0') // scale + assert.equal(snapshot[4], '0') // epoch + } + + const deposit_A = dec(1000, 18) + const deposit_B = dec(2000, 18) + const deposit_C = dec(3000, 18) + + // --- TEST --- + + // A, B, C provide to SP + const G1 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) + await stabilityPool.provideToSP(deposit_A, frontEnd_1, { from: A }) + + const G2 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) + await stabilityPool.provideToSP(deposit_B, frontEnd_2, { from: B }) + + const G3 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) + await stabilityPool.provideToSP(deposit_C, frontEnd_3, { from: C }) + + const frontEnds = [frontEnd_1, frontEnd_2, frontEnd_3] + const G_Values = [G1, G2, G3] + + // Map frontEnds to the value of G at time the deposit was made + frontEndToG = th.zipToObject(frontEnds, G_Values) + + // Get front ends' snapshots after + for (const [frontEnd, G] of Object.entries(frontEndToG)) { + const snapshot = await stabilityPool.frontEndSnapshots(frontEnd) + + // Check snapshots are the expected values + assert.equal(snapshot[0], '0') // S (should always be 0 for front ends) + assert.isTrue(snapshot[1].eq(P_Before)) // P + assert.isTrue(snapshot[2].eq(G)) // G + assert.equal(snapshot[3], '0') // scale + assert.equal(snapshot[4], '0') // epoch + } + }) + + it("provideToSP(), new deposit: depositor does not receive ETH gains", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // Whale transfers LUSD to A, B + await lusdToken.transfer(A, dec(100, 18), { from: whale }) + await lusdToken.transfer(B, dec(200, 18), { from: whale }) + + // C, D open troves + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) + + // --- TEST --- + + // get current ETH balances + const A_ETHBalance_Before = await web3.eth.getBalance(A) + const B_ETHBalance_Before = await web3.eth.getBalance(B) + const C_ETHBalance_Before = await web3.eth.getBalance(C) + const D_ETHBalance_Before = await web3.eth.getBalance(D) + + // A, B, C, D provide to SP + await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: A, gasPrice: 0 }) + await stabilityPool.provideToSP(dec(200, 18), ZERO_ADDRESS, { from: B, gasPrice: 0 }) + await stabilityPool.provideToSP(dec(300, 18), frontEnd_2, { from: C, gasPrice: 0 }) + await stabilityPool.provideToSP(dec(400, 18), ZERO_ADDRESS, { from: D, gasPrice: 0 }) + + // Get ETH balances after + const A_ETHBalance_After = await web3.eth.getBalance(A) + const B_ETHBalance_After = await web3.eth.getBalance(B) + const C_ETHBalance_After = await web3.eth.getBalance(C) + const D_ETHBalance_After = await web3.eth.getBalance(D) + + // Check ETH balances have not changed + assert.equal(A_ETHBalance_After, A_ETHBalance_Before) + assert.equal(B_ETHBalance_After, B_ETHBalance_Before) + assert.equal(C_ETHBalance_After, C_ETHBalance_Before) + assert.equal(D_ETHBalance_After, D_ETHBalance_Before) + }) + + it("provideToSP(), new deposit after past full withdrawal: depositor does not receive ETH gains", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // Whale transfers LUSD to A, B + await lusdToken.transfer(A, dec(1000, 18), { from: whale }) + await lusdToken.transfer(B, dec(1000, 18), { from: whale }) + + // C, D open troves + await openTrove({ extraLUSDAmount: toBN(dec(4000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + await openTrove({ extraLUSDAmount: toBN(dec(5000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) + + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // --- SETUP --- + // A, B, C, D provide to SP + await stabilityPool.provideToSP(dec(105, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(105, 18), ZERO_ADDRESS, { from: B }) + await stabilityPool.provideToSP(dec(105, 18), frontEnd_1, { from: C }) + await stabilityPool.provideToSP(dec(105, 18), ZERO_ADDRESS, { from: D }) + + // time passes + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // B deposits. A,B,C,D earn LQTY + await stabilityPool.provideToSP(dec(5, 18), ZERO_ADDRESS, { from: B }) + + // Price drops, defaulter is liquidated, A, B, C, D earn ETH + await priceFeed.setPrice(dec(105, 18)) + assert.isFalse(await th.checkRecoveryMode(contracts)) + + await troveManager.liquidate(defaulter_1) + + // Price bounces back + await priceFeed.setPrice(dec(200, 18)) + + // A B,C, D fully withdraw from the pool + await stabilityPool.withdrawFromSP(dec(105, 18), { from: A }) + await stabilityPool.withdrawFromSP(dec(105, 18), { from: B }) + await stabilityPool.withdrawFromSP(dec(105, 18), { from: C }) + await stabilityPool.withdrawFromSP(dec(105, 18), { from: D }) + + // --- TEST --- + + // get current ETH balances + const A_ETHBalance_Before = await web3.eth.getBalance(A) + const B_ETHBalance_Before = await web3.eth.getBalance(B) + const C_ETHBalance_Before = await web3.eth.getBalance(C) + const D_ETHBalance_Before = await web3.eth.getBalance(D) + + // A, B, C, D provide to SP + await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: A, gasPrice: 0 }) + await stabilityPool.provideToSP(dec(200, 18), ZERO_ADDRESS, { from: B, gasPrice: 0 }) + await stabilityPool.provideToSP(dec(300, 18), frontEnd_2, { from: C, gasPrice: 0 }) + await stabilityPool.provideToSP(dec(400, 18), ZERO_ADDRESS, { from: D, gasPrice: 0 }) + + // Get ETH balances after + const A_ETHBalance_After = await web3.eth.getBalance(A) + const B_ETHBalance_After = await web3.eth.getBalance(B) + const C_ETHBalance_After = await web3.eth.getBalance(C) + const D_ETHBalance_After = await web3.eth.getBalance(D) + + // Check ETH balances have not changed + assert.equal(A_ETHBalance_After, A_ETHBalance_Before) + assert.equal(B_ETHBalance_After, B_ETHBalance_Before) + assert.equal(C_ETHBalance_After, C_ETHBalance_Before) + assert.equal(D_ETHBalance_After, D_ETHBalance_Before) + }) + + it("provideToSP(), topup: triggers LQTY reward event - increases the sum G", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C open troves + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + + // A, B, C provide to SP + await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(50, 18), frontEnd_1, { from: B }) + await stabilityPool.provideToSP(dec(50, 18), frontEnd_1, { from: C }) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + const G_Before = await stabilityPool.epochToScaleToG(0, 0) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // B tops up + await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: B }) + + const G_After = await stabilityPool.epochToScaleToG(0, 0) + + // Expect G has increased from the LQTY reward event triggered by B's topup + assert.isTrue(G_After.gt(G_Before)) + }) + + it("provideToSP(), topup from different front end: doesn't change the front end tag", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // whale transfer to troves D and E + await lusdToken.transfer(D, dec(100, 18), { from: whale }) + await lusdToken.transfer(E, dec(200, 18), { from: whale }) + + // A, B, C open troves + await openTrove({ extraLUSDAmount: toBN(dec(100, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(200, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(300, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + + + // A, B, C, D, E provide to SP + await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: B }) + await stabilityPool.provideToSP(dec(30, 18), ZERO_ADDRESS, { from: C }) + await stabilityPool.provideToSP(dec(40, 18), frontEnd_1, { from: D }) + await stabilityPool.provideToSP(dec(50, 18), ZERO_ADDRESS, { from: E }) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // A, B, C, D, E top up, from different front ends + await stabilityPool.provideToSP(dec(10, 18), frontEnd_2, { from: A }) + await stabilityPool.provideToSP(dec(20, 18), frontEnd_1, { from: B }) + await stabilityPool.provideToSP(dec(15, 18), frontEnd_3, { from: C }) + await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: D }) + await stabilityPool.provideToSP(dec(30, 18), frontEnd_3, { from: E }) + + const frontEndTag_A = (await stabilityPool.deposits(A))[1] + const frontEndTag_B = (await stabilityPool.deposits(B))[1] + const frontEndTag_C = (await stabilityPool.deposits(C))[1] + const frontEndTag_D = (await stabilityPool.deposits(D))[1] + const frontEndTag_E = (await stabilityPool.deposits(E))[1] + + // Check deposits are still tagged with their original front end + assert.equal(frontEndTag_A, frontEnd_1) + assert.equal(frontEndTag_B, frontEnd_2) + assert.equal(frontEndTag_C, ZERO_ADDRESS) + assert.equal(frontEndTag_D, frontEnd_1) + assert.equal(frontEndTag_E, ZERO_ADDRESS) + }) + + it("provideToSP(), topup: depositor receives LQTY rewards", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C open troves + await openTrove({ extraLUSDAmount: toBN(dec(100, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(200, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(300, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + + // A, B, C, provide to SP + await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: B }) + await stabilityPool.provideToSP(dec(30, 18), ZERO_ADDRESS, { from: C }) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // Get A, B, C LQTY balance before + const A_LQTYBalance_Before = await lqtyToken.balanceOf(A) + const B_LQTYBalance_Before = await lqtyToken.balanceOf(B) + const C_LQTYBalance_Before = await lqtyToken.balanceOf(C) + + // A, B, C top up + await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: B }) + await stabilityPool.provideToSP(dec(30, 18), ZERO_ADDRESS, { from: C }) + + // Get LQTY balance after + const A_LQTYBalance_After = await lqtyToken.balanceOf(A) + const B_LQTYBalance_After = await lqtyToken.balanceOf(B) + const C_LQTYBalance_After = await lqtyToken.balanceOf(C) + + // Check LQTY Balance of A, B, C has increased + assert.isTrue(A_LQTYBalance_After.gt(A_LQTYBalance_Before)) + assert.isTrue(B_LQTYBalance_After.gt(B_LQTYBalance_Before)) + assert.isTrue(C_LQTYBalance_After.gt(C_LQTYBalance_Before)) + }) + + it("provideToSP(), topup: tagged front end receives LQTY rewards", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C open troves + await openTrove({ extraLUSDAmount: toBN(dec(100, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(200, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(300, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + + // A, B, C, provide to SP + await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: B }) + await stabilityPool.provideToSP(dec(30, 18), frontEnd_3, { from: C }) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // Get front ends' LQTY balance before + const F1_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_1) + const F2_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_2) + const F3_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_3) + + // A, B, C top up (front end param passed here is irrelevant) + await stabilityPool.provideToSP(dec(10, 18), ZERO_ADDRESS, { from: A }) // provides no front end param + await stabilityPool.provideToSP(dec(20, 18), frontEnd_1, { from: B }) // provides front end that doesn't match his tag + await stabilityPool.provideToSP(dec(30, 18), frontEnd_3, { from: C }) // provides front end that matches his tag + + // Get front ends' LQTY balance after + const F1_LQTYBalance_After = await lqtyToken.balanceOf(A) + const F2_LQTYBalance_After = await lqtyToken.balanceOf(B) + const F3_LQTYBalance_After = await lqtyToken.balanceOf(C) + + // Check LQTY Balance of front ends has increased + assert.isTrue(F1_LQTYBalance_After.gt(F1_LQTYBalance_Before)) + assert.isTrue(F2_LQTYBalance_After.gt(F2_LQTYBalance_Before)) + assert.isTrue(F3_LQTYBalance_After.gt(F3_LQTYBalance_Before)) + }) + + it("provideToSP(), topup: tagged front end's stake increases", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C, D, E, F open troves + await openTrove({ extraLUSDAmount: toBN(dec(100, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(200, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(300, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + await openTrove({ extraLUSDAmount: toBN(dec(100, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) + await openTrove({ extraLUSDAmount: toBN(dec(200, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: E } }) + await openTrove({ extraLUSDAmount: toBN(dec(300, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: F } }) + + // A, B, C, D, E, F provide to SP + await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: B }) + await stabilityPool.provideToSP(dec(30, 18), frontEnd_3, { from: C }) + await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: D }) + await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: E }) + await stabilityPool.provideToSP(dec(30, 18), frontEnd_3, { from: F }) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // Get front ends' stake before + const F1_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_1) + const F2_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_2) + const F3_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_3) + + // A, B, C top up (front end param passed here is irrelevant) + await stabilityPool.provideToSP(dec(10, 18), ZERO_ADDRESS, { from: A }) // provides no front end param + await stabilityPool.provideToSP(dec(20, 18), frontEnd_1, { from: B }) // provides front end that doesn't match his tag + await stabilityPool.provideToSP(dec(30, 18), frontEnd_3, { from: C }) // provides front end that matches his tag + + // Get front ends' stakes after + const F1_Stake_After = await stabilityPool.frontEndStakes(frontEnd_1) + const F2_Stake_After = await stabilityPool.frontEndStakes(frontEnd_2) + const F3_Stake_After = await stabilityPool.frontEndStakes(frontEnd_3) + + // Check front ends' stakes have increased + assert.isTrue(F1_Stake_After.gt(F1_Stake_Before)) + assert.isTrue(F2_Stake_After.gt(F2_Stake_Before)) + assert.isTrue(F3_Stake_After.gt(F3_Stake_Before)) + }) + + it("provideToSP(), topup: tagged front end's snapshots update", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C, open troves + await openTrove({ extraLUSDAmount: toBN(dec(200, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(400, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(600, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + + // D opens trove + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) + + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // --- SETUP --- + + const deposit_A = dec(100, 18) + const deposit_B = dec(200, 18) + const deposit_C = dec(300, 18) + + // A, B, C make their initial deposits + await stabilityPool.provideToSP(deposit_A, frontEnd_1, { from: A }) + await stabilityPool.provideToSP(deposit_B, frontEnd_2, { from: B }) + await stabilityPool.provideToSP(deposit_C, frontEnd_3, { from: C }) + + // fastforward time then make an SP deposit, to make G > 0 + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + await stabilityPool.provideToSP(await lusdToken.balanceOf(D), ZERO_ADDRESS, { from: D }) + + // perform a liquidation to make 0 < P < 1, and S > 0 + await priceFeed.setPrice(dec(100, 18)) + assert.isFalse(await th.checkRecoveryMode(contracts)) + + await troveManager.liquidate(defaulter_1) + + const currentEpoch = await stabilityPool.currentEpoch() + const currentScale = await stabilityPool.currentScale() + + const S_Before = await stabilityPool.epochToScaleToSum(currentEpoch, currentScale) + const P_Before = await stabilityPool.P() + const G_Before = await stabilityPool.epochToScaleToG(currentEpoch, currentScale) + + // Confirm 0 < P < 1 + assert.isTrue(P_Before.gt(toBN('0')) && P_Before.lt(toBN(dec(1, 18)))) + // Confirm S, G are both > 0 + assert.isTrue(S_Before.gt(toBN('0'))) + assert.isTrue(G_Before.gt(toBN('0'))) + + // Get front ends' snapshots before + for (frontEnd of [frontEnd_1, frontEnd_2, frontEnd_3]) { + const snapshot = await stabilityPool.frontEndSnapshots(frontEnd) + + assert.equal(snapshot[0], '0') // S (should always be 0 for front ends, since S corresponds to ETH gain) + assert.equal(snapshot[1], dec(1, 18)) // P + assert.equal(snapshot[2], '0') // G + assert.equal(snapshot[3], '0') // scale + assert.equal(snapshot[4], '0') // epoch + } + + // --- TEST --- + + // A, B, C top up their deposits. Grab G at each stage, as it can increase a bit + // between topups, because some block.timestamp time passes (and LQTY is issued) between ops + const G1 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) + await stabilityPool.provideToSP(deposit_A, frontEnd_1, { from: A }) + + const G2 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) + await stabilityPool.provideToSP(deposit_B, frontEnd_2, { from: B }) + + const G3 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) + await stabilityPool.provideToSP(deposit_C, frontEnd_3, { from: C }) + + const frontEnds = [frontEnd_1, frontEnd_2, frontEnd_3] + const G_Values = [G1, G2, G3] + + // Map frontEnds to the value of G at time the deposit was made + frontEndToG = th.zipToObject(frontEnds, G_Values) + + // Get front ends' snapshots after + for (const [frontEnd, G] of Object.entries(frontEndToG)) { + const snapshot = await stabilityPool.frontEndSnapshots(frontEnd) + + // Check snapshots are the expected values + assert.equal(snapshot[0], '0') // S (should always be 0 for front ends) + assert.isTrue(snapshot[1].eq(P_Before)) // P + assert.isTrue(snapshot[2].eq(G)) // G + assert.equal(snapshot[3], '0') // scale + assert.equal(snapshot[4], '0') // epoch + } + }) + + it("provideToSP(): reverts when amount is zero", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + + // Whale transfers LUSD to C, D + await lusdToken.transfer(C, dec(100, 18), { from: whale }) + await lusdToken.transfer(D, dec(100, 18), { from: whale }) + + txPromise_A = stabilityPool.provideToSP(0, frontEnd_1, { from: A }) + txPromise_B = stabilityPool.provideToSP(0, ZERO_ADDRESS, { from: B }) + txPromise_C = stabilityPool.provideToSP(0, frontEnd_2, { from: C }) + txPromise_D = stabilityPool.provideToSP(0, ZERO_ADDRESS, { from: D }) + + await th.assertRevert(txPromise_A, 'StabilityPool: Amount must be non-zero') + await th.assertRevert(txPromise_B, 'StabilityPool: Amount must be non-zero') + await th.assertRevert(txPromise_C, 'StabilityPool: Amount must be non-zero') + await th.assertRevert(txPromise_D, 'StabilityPool: Amount must be non-zero') + }) + + it("provideToSP(): reverts if user is a registered front end", async () => { + // C, D, E, F open troves + await openTrove({ extraLUSDAmount: toBN(dec(30, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + await openTrove({ extraLUSDAmount: toBN(dec(30, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) + await openTrove({ extraLUSDAmount: toBN(dec(30, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: E } }) + await openTrove({ extraLUSDAmount: toBN(dec(30, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: F } }) + + // C, E, F registers as front end + await stabilityPool.registerFrontEnd(dec(1, 18), { from: C }) + await stabilityPool.registerFrontEnd(dec(1, 18), { from: E }) + await stabilityPool.registerFrontEnd(dec(1, 18), { from: F }) + + const txPromise_C = stabilityPool.provideToSP(dec(10, 18), ZERO_ADDRESS, { from: C }) + const txPromise_E = stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: E }) + const txPromise_F = stabilityPool.provideToSP(dec(10, 18), F, { from: F }) + await th.assertRevert(txPromise_C, "StabilityPool: must not already be a registered front end") + await th.assertRevert(txPromise_E, "StabilityPool: must not already be a registered front end") + await th.assertRevert(txPromise_F, "StabilityPool: must not already be a registered front end") + + const txD = await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: D }) + assert.isTrue(txD.receipt.status) + }) + + it("provideToSP(): reverts if provided tag is not a registered front end", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(30, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + await openTrove({ extraLUSDAmount: toBN(dec(30, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) + await openTrove({ extraLUSDAmount: toBN(dec(30, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: E } }) + + const txPromise_C = stabilityPool.provideToSP(dec(10, 18), A, { from: C }) // passes another EOA + const txPromise_D = stabilityPool.provideToSP(dec(10, 18), troveManager.address, { from: D }) + const txPromise_E = stabilityPool.provideToSP(dec(10, 18), stabilityPool.address, { from: E }) + const txPromise_F = stabilityPool.provideToSP(dec(10, 18), F, { from: F }) // passes itself + + await th.assertRevert(txPromise_C, "StabilityPool: Tag must be a registered front end, or the zero address") + await th.assertRevert(txPromise_D, "StabilityPool: Tag must be a registered front end, or the zero address") + await th.assertRevert(txPromise_E, "StabilityPool: Tag must be a registered front end, or the zero address") + await th.assertRevert(txPromise_F, "StabilityPool: Tag must be a registered front end, or the zero address") + }) + + // --- withdrawFromSP --- + + it("withdrawFromSP(): reverts when user has no active deposit", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(100, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await openTrove({ extraLUSDAmount: toBN(dec(100, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) + + await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: alice }) + + const alice_initialDeposit = ((await stabilityPool.deposits(alice))[0]).toString() + const bob_initialDeposit = ((await stabilityPool.deposits(bob))[0]).toString() + + assert.equal(alice_initialDeposit, dec(100, 18)) + assert.equal(bob_initialDeposit, '0') + + const txAlice = await stabilityPool.withdrawFromSP(dec(100, 18), { from: alice }) + assert.isTrue(txAlice.receipt.status) + + + try { + const txBob = await stabilityPool.withdrawFromSP(dec(100, 18), { from: bob }) + assert.isFalse(txBob.receipt.status) + } catch (err) { + assert.include(err.message, "revert") + // TODO: infamous issue #99 + //assert.include(err.message, "User must have a non-zero deposit") + + } + }) + + it("withdrawFromSP(): reverts when amount > 0 and system has an undercollateralized trove", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(100, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + + await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: alice }) + + const alice_initialDeposit = ((await stabilityPool.deposits(alice))[0]).toString() + assert.equal(alice_initialDeposit, dec(100, 18)) + + // defaulter opens trove + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // ETH drops, defaulter is in liquidation range (but not liquidated yet) + await priceFeed.setPrice(dec(100, 18)) + + await th.assertRevert(stabilityPool.withdrawFromSP(dec(100, 18), { from: alice })) + }) + + it("withdrawFromSP(): partial retrieval - retrieves correct LUSD amount and the entire ETH Gain, and updates deposit", async () => { + // --- SETUP --- + // Whale deposits 185000 LUSD in StabilityPool + await openTrove({ extraLUSDAmount: toBN(dec(1, 24)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + await stabilityPool.provideToSP(dec(185000, 18), frontEnd_1, { from: whale }) + + // 2 Troves opened + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) + + // --- TEST --- + + // Alice makes deposit #1: 15000 LUSD + await openTrove({ extraLUSDAmount: toBN(dec(15000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: alice } }) + await stabilityPool.provideToSP(dec(15000, 18), frontEnd_1, { from: alice }) + + // price drops: defaulters' Troves fall below MCR, alice and whale Trove remain active + await priceFeed.setPrice(dec(105, 18)); + + // 2 users with Trove with 170 LUSD drawn are closed + const liquidationTX_1 = await troveManager.liquidate(defaulter_1, { from: owner }) // 170 LUSD closed + const liquidationTX_2 = await troveManager.liquidate(defaulter_2, { from: owner }) // 170 LUSD closed + + const [liquidatedDebt_1] = await th.getEmittedLiquidationValues(liquidationTX_1) + const [liquidatedDebt_2] = await th.getEmittedLiquidationValues(liquidationTX_2) + + // Alice LUSDLoss is ((15000/200000) * liquidatedDebt), for each liquidation + const expectedLUSDLoss_A = (liquidatedDebt_1.mul(toBN(dec(15000, 18))).div(toBN(dec(200000, 18)))) + .add(liquidatedDebt_2.mul(toBN(dec(15000, 18))).div(toBN(dec(200000, 18)))) + + const expectedCompoundedLUSDDeposit_A = toBN(dec(15000, 18)).sub(expectedLUSDLoss_A) + const compoundedLUSDDeposit_A = await stabilityPool.getCompoundedLUSDDeposit(alice) + + assert.isAtMost(th.getDifference(expectedCompoundedLUSDDeposit_A, compoundedLUSDDeposit_A), 100000) + + // Alice retrieves part of her entitled LUSD: 9000 LUSD + await stabilityPool.withdrawFromSP(dec(9000, 18), { from: alice }) + + const expectedNewDeposit_A = (compoundedLUSDDeposit_A.sub(toBN(dec(9000, 18)))) + + // check Alice's deposit has been updated to equal her compounded deposit minus her withdrawal */ + const newDeposit = ((await stabilityPool.deposits(alice))[0]).toString() + assert.isAtMost(th.getDifference(newDeposit, expectedNewDeposit_A), 100000) + + // Expect Alice has withdrawn all ETH gain + const alice_pendingETHGain = await stabilityPool.getDepositorETHGain(alice) + assert.equal(alice_pendingETHGain, 0) + }) + + it("withdrawFromSP(): partial retrieval - leaves the correct amount of LUSD in the Stability Pool", async () => { + // --- SETUP --- + // Whale deposits 185000 LUSD in StabilityPool + await openTrove({ extraLUSDAmount: toBN(dec(1, 24)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + await stabilityPool.provideToSP(dec(185000, 18), frontEnd_1, { from: whale }) + + // 2 Troves opened + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) + // --- TEST --- + + // Alice makes deposit #1: 15000 LUSD + await openTrove({ extraLUSDAmount: toBN(dec(15000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: alice } }) + await stabilityPool.provideToSP(dec(15000, 18), frontEnd_1, { from: alice }) + + const SP_LUSD_Before = await stabilityPool.getTotalLUSDDeposits() + assert.equal(SP_LUSD_Before, dec(200000, 18)) + + // price drops: defaulters' Troves fall below MCR, alice and whale Trove remain active + await priceFeed.setPrice(dec(105, 18)); + + // 2 users liquidated + const liquidationTX_1 = await troveManager.liquidate(defaulter_1, { from: owner }) + const liquidationTX_2 = await troveManager.liquidate(defaulter_2, { from: owner }) + + const [liquidatedDebt_1] = await th.getEmittedLiquidationValues(liquidationTX_1) + const [liquidatedDebt_2] = await th.getEmittedLiquidationValues(liquidationTX_2) + + // Alice retrieves part of her entitled LUSD: 9000 LUSD + await stabilityPool.withdrawFromSP(dec(9000, 18), { from: alice }) + + /* Check SP has reduced from 2 liquidations and Alice's withdrawal + Expect LUSD in SP = (200000 - liquidatedDebt_1 - liquidatedDebt_2 - 9000) */ + const expectedSPLUSD = toBN(dec(200000, 18)) + .sub(toBN(liquidatedDebt_1)) + .sub(toBN(liquidatedDebt_2)) + .sub(toBN(dec(9000, 18))) + + const SP_LUSD_After = (await stabilityPool.getTotalLUSDDeposits()).toString() + + th.assertIsApproximatelyEqual(SP_LUSD_After, expectedSPLUSD) + }) + + it("withdrawFromSP(): full retrieval - leaves the correct amount of LUSD in the Stability Pool", async () => { + // --- SETUP --- + // Whale deposits 185000 LUSD in StabilityPool + await openTrove({ extraLUSDAmount: toBN(dec(1000000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + await stabilityPool.provideToSP(dec(185000, 18), frontEnd_1, { from: whale }) + + // 2 Troves opened + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) + + // --- TEST --- + + // Alice makes deposit #1 + await openTrove({ extraLUSDAmount: toBN(dec(15000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: alice } }) + await stabilityPool.provideToSP(dec(15000, 18), frontEnd_1, { from: alice }) + + const SP_LUSD_Before = await stabilityPool.getTotalLUSDDeposits() + assert.equal(SP_LUSD_Before, dec(200000, 18)) + + // price drops: defaulters' Troves fall below MCR, alice and whale Trove remain active + await priceFeed.setPrice(dec(105, 18)); + + // 2 defaulters liquidated + const liquidationTX_1 = await troveManager.liquidate(defaulter_1, { from: owner }) + const liquidationTX_2 = await troveManager.liquidate(defaulter_2, { from: owner }) + + const [liquidatedDebt_1] = await th.getEmittedLiquidationValues(liquidationTX_1) + const [liquidatedDebt_2] = await th.getEmittedLiquidationValues(liquidationTX_2) + + // Alice LUSDLoss is ((15000/200000) * liquidatedDebt), for each liquidation + const expectedLUSDLoss_A = (liquidatedDebt_1.mul(toBN(dec(15000, 18))).div(toBN(dec(200000, 18)))) + .add(liquidatedDebt_2.mul(toBN(dec(15000, 18))).div(toBN(dec(200000, 18)))) + + const expectedCompoundedLUSDDeposit_A = toBN(dec(15000, 18)).sub(expectedLUSDLoss_A) + const compoundedLUSDDeposit_A = await stabilityPool.getCompoundedLUSDDeposit(alice) + + assert.isAtMost(th.getDifference(expectedCompoundedLUSDDeposit_A, compoundedLUSDDeposit_A), 100000) + + const LUSDinSPBefore = await stabilityPool.getTotalLUSDDeposits() + + // Alice retrieves all of her entitled LUSD: + await stabilityPool.withdrawFromSP(dec(15000, 18), { from: alice }) + + const expectedLUSDinSPAfter = LUSDinSPBefore.sub(compoundedLUSDDeposit_A) + + const LUSDinSPAfter = await stabilityPool.getTotalLUSDDeposits() + assert.isAtMost(th.getDifference(expectedLUSDinSPAfter, LUSDinSPAfter), 100000) + }) + + it("withdrawFromSP(): Subsequent deposit and withdrawal attempt from same account, with no intermediate liquidations, withdraws zero ETH", async () => { + // --- SETUP --- + // Whale deposits 1850 LUSD in StabilityPool + await openTrove({ extraLUSDAmount: toBN(dec(1000000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + await stabilityPool.provideToSP(dec(18500, 18), frontEnd_1, { from: whale }) + + // 2 defaulters open + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) + + // --- TEST --- + + // Alice makes deposit #1: 15000 LUSD + await openTrove({ extraLUSDAmount: toBN(dec(15000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: alice } }) + await stabilityPool.provideToSP(dec(15000, 18), frontEnd_1, { from: alice }) + + // price drops: defaulters' Troves fall below MCR, alice and whale Trove remain active + await priceFeed.setPrice(dec(105, 18)); + + // defaulters liquidated + await troveManager.liquidate(defaulter_1, { from: owner }) + await troveManager.liquidate(defaulter_2, { from: owner }) + + // Alice retrieves all of her entitled LUSD: + await stabilityPool.withdrawFromSP(dec(15000, 18), { from: alice }) + assert.equal(await stabilityPool.getDepositorETHGain(alice), 0) + + // Alice makes second deposit + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: alice }) + assert.equal(await stabilityPool.getDepositorETHGain(alice), 0) + + const ETHinSP_Before = (await stabilityPool.getETH()).toString() + + // Alice attempts second withdrawal + await stabilityPool.withdrawFromSP(dec(10000, 18), { from: alice }) + assert.equal(await stabilityPool.getDepositorETHGain(alice), 0) + + // Check ETH in pool does not change + const ETHinSP_1 = (await stabilityPool.getETH()).toString() + assert.equal(ETHinSP_Before, ETHinSP_1) + + // Third deposit + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: alice }) + assert.equal(await stabilityPool.getDepositorETHGain(alice), 0) + + // Alice attempts third withdrawal (this time, frm SP to Trove) + const txPromise_A = stabilityPool.withdrawETHGainToTrove(alice, alice, { from: alice }) + await th.assertRevert(txPromise_A) + }) + + it("withdrawFromSP(): it correctly updates the user's LUSD and ETH snapshots of entitled reward per unit staked", async () => { + // --- SETUP --- + // Whale deposits 185000 LUSD in StabilityPool + await openTrove({ extraLUSDAmount: toBN(dec(1000000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + await stabilityPool.provideToSP(dec(185000, 18), frontEnd_1, { from: whale }) + + // 2 defaulters open + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) + + // --- TEST --- + + // Alice makes deposit #1: 15000 LUSD + await openTrove({ extraLUSDAmount: toBN(dec(15000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: alice } }) + await stabilityPool.provideToSP(dec(15000, 18), frontEnd_1, { from: alice }) + + // check 'Before' snapshots + const alice_snapshot_Before = await stabilityPool.depositSnapshots(alice) + const alice_snapshot_S_Before = alice_snapshot_Before[0].toString() + const alice_snapshot_P_Before = alice_snapshot_Before[1].toString() + assert.equal(alice_snapshot_S_Before, 0) + assert.equal(alice_snapshot_P_Before, '1000000000000000000') + + // price drops: defaulters' Troves fall below MCR, alice and whale Trove remain active + await priceFeed.setPrice(dec(105, 18)); + + // 2 defaulters liquidated + await troveManager.liquidate(defaulter_1, { from: owner }) + await troveManager.liquidate(defaulter_2, { from: owner }); + + // Alice retrieves part of her entitled LUSD: 9000 LUSD + await stabilityPool.withdrawFromSP(dec(9000, 18), { from: alice }) + + const P = (await stabilityPool.P()).toString() + const S = (await stabilityPool.epochToScaleToSum(0, 0)).toString() + // check 'After' snapshots + const alice_snapshot_After = await stabilityPool.depositSnapshots(alice) + const alice_snapshot_S_After = alice_snapshot_After[0].toString() + const alice_snapshot_P_After = alice_snapshot_After[1].toString() + assert.equal(alice_snapshot_S_After, S) + assert.equal(alice_snapshot_P_After, P) + }) + + it("withdrawFromSP(): decreases StabilityPool ETH", async () => { + // --- SETUP --- + // Whale deposits 185000 LUSD in StabilityPool + await openTrove({ extraLUSDAmount: toBN(dec(1000000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + await stabilityPool.provideToSP(dec(185000, 18), frontEnd_1, { from: whale }) + + // 1 defaulter opens + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // --- TEST --- + + // Alice makes deposit #1: 15000 LUSD + await openTrove({ extraLUSDAmount: toBN(dec(15000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: alice } }) + await stabilityPool.provideToSP(dec(15000, 18), frontEnd_1, { from: alice }) + + // price drops: defaulter's Trove falls below MCR, alice and whale Trove remain active + await priceFeed.setPrice('100000000000000000000'); + + // defaulter's Trove is closed. + const liquidationTx_1 = await troveManager.liquidate(defaulter_1, { from: owner }) // 180 LUSD closed + const [, liquidatedColl,] = th.getEmittedLiquidationValues(liquidationTx_1) + + //Get ActivePool and StabilityPool Ether before retrieval: + const active_ETH_Before = await activePool.getETH() + const stability_ETH_Before = await stabilityPool.getETH() + + // Expect alice to be entitled to 15000/200000 of the liquidated coll + const aliceExpectedETHGain = liquidatedColl.mul(toBN(dec(15000, 18))).div(toBN(dec(200000, 18))) + const aliceETHGain = await stabilityPool.getDepositorETHGain(alice) + assert.isTrue(aliceExpectedETHGain.eq(aliceETHGain)) + + // Alice retrieves all of her deposit + await stabilityPool.withdrawFromSP(dec(15000, 18), { from: alice }) + + const active_ETH_After = await activePool.getETH() + const stability_ETH_After = await stabilityPool.getETH() + + const active_ETH_Difference = (active_ETH_Before.sub(active_ETH_After)) + const stability_ETH_Difference = (stability_ETH_Before.sub(stability_ETH_After)) + + assert.equal(active_ETH_Difference, '0') + + // Expect StabilityPool to have decreased by Alice's ETHGain + assert.isAtMost(th.getDifference(stability_ETH_Difference, aliceETHGain), 10000) + }) + + it("withdrawFromSP(): All depositors are able to withdraw from the SP to their account", async () => { + // Whale opens trove + await openTrove({ ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // 1 defaulter open + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // 6 Accounts open troves and provide to SP + const depositors = [alice, bob, carol, dennis, erin, flyn] + for (account of depositors) { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: account } }) + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: account }) + } + + await priceFeed.setPrice(dec(105, 18)) + await troveManager.liquidate(defaulter_1) + + await priceFeed.setPrice(dec(200, 18)) + + // All depositors attempt to withdraw + await stabilityPool.withdrawFromSP(dec(10000, 18), { from: alice }) + assert.equal(((await stabilityPool.deposits(alice))[0]).toString(), '0') + await stabilityPool.withdrawFromSP(dec(10000, 18), { from: bob }) + assert.equal(((await stabilityPool.deposits(alice))[0]).toString(), '0') + await stabilityPool.withdrawFromSP(dec(10000, 18), { from: carol }) + assert.equal(((await stabilityPool.deposits(alice))[0]).toString(), '0') + await stabilityPool.withdrawFromSP(dec(10000, 18), { from: dennis }) + assert.equal(((await stabilityPool.deposits(alice))[0]).toString(), '0') + await stabilityPool.withdrawFromSP(dec(10000, 18), { from: erin }) + assert.equal(((await stabilityPool.deposits(alice))[0]).toString(), '0') + await stabilityPool.withdrawFromSP(dec(10000, 18), { from: flyn }) + assert.equal(((await stabilityPool.deposits(alice))[0]).toString(), '0') + + const totalDeposits = (await stabilityPool.getTotalLUSDDeposits()).toString() + + assert.isAtMost(th.getDifference(totalDeposits, '0'), 100000) + }) + + it("withdrawFromSP(): increases depositor's LUSD token balance by the expected amount", async () => { + // Whale opens trove + await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // 1 defaulter opens trove + await borrowerOperations.openTrove(th._100pct, await getOpenTroveLUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }) + + const defaulterDebt = (await troveManager.getEntireDebtAndColl(defaulter_1))[0] + + // 6 Accounts open troves and provide to SP + const depositors = [alice, bob, carol, dennis, erin, flyn] + for (account of depositors) { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: account } }) + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: account }) + } + + await priceFeed.setPrice(dec(105, 18)) + await troveManager.liquidate(defaulter_1) + + const aliceBalBefore = await lusdToken.balanceOf(alice) + const bobBalBefore = await lusdToken.balanceOf(bob) + + /* From an offset of 10000 LUSD, each depositor receives + LUSDLoss = 1666.6666666666666666 LUSD + + and thus with a deposit of 10000 LUSD, each should withdraw 8333.3333333333333333 LUSD (in practice, slightly less due to rounding error) + */ + + // Price bounces back to $200 per ETH + await priceFeed.setPrice(dec(200, 18)) + + // Bob issues a further 5000 LUSD from his trove + await borrowerOperations.withdrawLUSD(th._100pct, dec(5000, 18), bob, bob, { from: bob }) + + // Expect Alice's LUSD balance increase be very close to 8333.3333333333333333 LUSD + await stabilityPool.withdrawFromSP(dec(10000, 18), { from: alice }) + const aliceBalance = (await lusdToken.balanceOf(alice)) + + assert.isAtMost(th.getDifference(aliceBalance.sub(aliceBalBefore), '8333333333333333333333'), 100000) + + // expect Bob's LUSD balance increase to be very close to 13333.33333333333333333 LUSD + await stabilityPool.withdrawFromSP(dec(10000, 18), { from: bob }) + const bobBalance = (await lusdToken.balanceOf(bob)) + assert.isAtMost(th.getDifference(bobBalance.sub(bobBalBefore), '13333333333333333333333'), 100000) + }) + + it("withdrawFromSP(): doesn't impact other users Stability deposits or ETH gains", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C open troves and make Stability Pool deposits + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) + + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: alice }) + await stabilityPool.provideToSP(dec(20000, 18), frontEnd_1, { from: bob }) + await stabilityPool.provideToSP(dec(30000, 18), frontEnd_1, { from: carol }) + + // Would-be defaulters open troves + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) + + // Price drops + await priceFeed.setPrice(dec(105, 18)) + + // Defaulters are liquidated + await troveManager.liquidate(defaulter_1) + await troveManager.liquidate(defaulter_2) + assert.isFalse(await sortedTroves.contains(defaulter_1)) + assert.isFalse(await sortedTroves.contains(defaulter_2)) + + const alice_LUSDDeposit_Before = (await stabilityPool.getCompoundedLUSDDeposit(alice)).toString() + const bob_LUSDDeposit_Before = (await stabilityPool.getCompoundedLUSDDeposit(bob)).toString() + + const alice_ETHGain_Before = (await stabilityPool.getDepositorETHGain(alice)).toString() + const bob_ETHGain_Before = (await stabilityPool.getDepositorETHGain(bob)).toString() + + //check non-zero LUSD and ETHGain in the Stability Pool + const LUSDinSP = await stabilityPool.getTotalLUSDDeposits() + const ETHinSP = await stabilityPool.getETH() + assert.isTrue(LUSDinSP.gt(mv._zeroBN)) + assert.isTrue(ETHinSP.gt(mv._zeroBN)) + + // Price rises + await priceFeed.setPrice(dec(200, 18)) + + // Carol withdraws her Stability deposit + assert.equal(((await stabilityPool.deposits(carol))[0]).toString(), dec(30000, 18)) + await stabilityPool.withdrawFromSP(dec(30000, 18), { from: carol }) + assert.equal(((await stabilityPool.deposits(carol))[0]).toString(), '0') + + const alice_LUSDDeposit_After = (await stabilityPool.getCompoundedLUSDDeposit(alice)).toString() + const bob_LUSDDeposit_After = (await stabilityPool.getCompoundedLUSDDeposit(bob)).toString() + + const alice_ETHGain_After = (await stabilityPool.getDepositorETHGain(alice)).toString() + const bob_ETHGain_After = (await stabilityPool.getDepositorETHGain(bob)).toString() + + // Check compounded deposits and ETH gains for A and B have not changed + assert.equal(alice_LUSDDeposit_Before, alice_LUSDDeposit_After) + assert.equal(bob_LUSDDeposit_Before, bob_LUSDDeposit_After) + + assert.equal(alice_ETHGain_Before, alice_ETHGain_After) + assert.equal(bob_ETHGain_Before, bob_ETHGain_After) + }) + + it("withdrawFromSP(): doesn't impact system debt, collateral or TCR ", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C open troves and make Stability Pool deposits + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) + + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: alice }) + await stabilityPool.provideToSP(dec(20000, 18), frontEnd_1, { from: bob }) + await stabilityPool.provideToSP(dec(30000, 18), frontEnd_1, { from: carol }) + + // Would-be defaulters open troves + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) + + // Price drops + await priceFeed.setPrice(dec(105, 18)) + + // Defaulters are liquidated + await troveManager.liquidate(defaulter_1) + await troveManager.liquidate(defaulter_2) + assert.isFalse(await sortedTroves.contains(defaulter_1)) + assert.isFalse(await sortedTroves.contains(defaulter_2)) + + // Price rises + await priceFeed.setPrice(dec(200, 18)) + + const activeDebt_Before = (await activePool.getLUSDDebt()).toString() + const defaultedDebt_Before = (await defaultPool.getLUSDDebt()).toString() + const activeColl_Before = (await activePool.getETH()).toString() + const defaultedColl_Before = (await defaultPool.getETH()).toString() + const TCR_Before = (await th.getTCR(contracts)).toString() + + // Carol withdraws her Stability deposit + assert.equal(((await stabilityPool.deposits(carol))[0]).toString(), dec(30000, 18)) + await stabilityPool.withdrawFromSP(dec(30000, 18), { from: carol }) + assert.equal(((await stabilityPool.deposits(carol))[0]).toString(), '0') + + const activeDebt_After = (await activePool.getLUSDDebt()).toString() + const defaultedDebt_After = (await defaultPool.getLUSDDebt()).toString() + const activeColl_After = (await activePool.getETH()).toString() + const defaultedColl_After = (await defaultPool.getETH()).toString() + const TCR_After = (await th.getTCR(contracts)).toString() + + // Check total system debt, collateral and TCR have not changed after a Stability deposit is made + assert.equal(activeDebt_Before, activeDebt_After) + assert.equal(defaultedDebt_Before, defaultedDebt_After) + assert.equal(activeColl_Before, activeColl_After) + assert.equal(defaultedColl_Before, defaultedColl_After) + assert.equal(TCR_Before, TCR_After) + }) + + it("withdrawFromSP(): doesn't impact any troves, including the caller's trove", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C open troves and make Stability Pool deposits + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) + + // A, B and C provide to SP + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: alice }) + await stabilityPool.provideToSP(dec(20000, 18), frontEnd_1, { from: bob }) + await stabilityPool.provideToSP(dec(30000, 18), frontEnd_1, { from: carol }) + + // Price drops + await priceFeed.setPrice(dec(105, 18)) + const price = await priceFeed.getPrice() + + // Get debt, collateral and ICR of all existing troves + const whale_Debt_Before = (await troveManager.Troves(whale))[0].toString() + const alice_Debt_Before = (await troveManager.Troves(alice))[0].toString() + const bob_Debt_Before = (await troveManager.Troves(bob))[0].toString() + const carol_Debt_Before = (await troveManager.Troves(carol))[0].toString() + + const whale_Coll_Before = (await troveManager.Troves(whale))[1].toString() + const alice_Coll_Before = (await troveManager.Troves(alice))[1].toString() + const bob_Coll_Before = (await troveManager.Troves(bob))[1].toString() + const carol_Coll_Before = (await troveManager.Troves(carol))[1].toString() + + const whale_ICR_Before = (await troveManager.getCurrentICR(whale, price)).toString() + const alice_ICR_Before = (await troveManager.getCurrentICR(alice, price)).toString() + const bob_ICR_Before = (await troveManager.getCurrentICR(bob, price)).toString() + const carol_ICR_Before = (await troveManager.getCurrentICR(carol, price)).toString() + + // price rises + await priceFeed.setPrice(dec(200, 18)) + + // Carol withdraws her Stability deposit + assert.equal(((await stabilityPool.deposits(carol))[0]).toString(), dec(30000, 18)) + await stabilityPool.withdrawFromSP(dec(30000, 18), { from: carol }) + assert.equal(((await stabilityPool.deposits(carol))[0]).toString(), '0') + + const whale_Debt_After = (await troveManager.Troves(whale))[0].toString() + const alice_Debt_After = (await troveManager.Troves(alice))[0].toString() + const bob_Debt_After = (await troveManager.Troves(bob))[0].toString() + const carol_Debt_After = (await troveManager.Troves(carol))[0].toString() + + const whale_Coll_After = (await troveManager.Troves(whale))[1].toString() + const alice_Coll_After = (await troveManager.Troves(alice))[1].toString() + const bob_Coll_After = (await troveManager.Troves(bob))[1].toString() + const carol_Coll_After = (await troveManager.Troves(carol))[1].toString() + + const whale_ICR_After = (await troveManager.getCurrentICR(whale, price)).toString() + const alice_ICR_After = (await troveManager.getCurrentICR(alice, price)).toString() + const bob_ICR_After = (await troveManager.getCurrentICR(bob, price)).toString() + const carol_ICR_After = (await troveManager.getCurrentICR(carol, price)).toString() + + // Check all troves are unaffected by Carol's Stability deposit withdrawal + assert.equal(whale_Debt_Before, whale_Debt_After) + assert.equal(alice_Debt_Before, alice_Debt_After) + assert.equal(bob_Debt_Before, bob_Debt_After) + assert.equal(carol_Debt_Before, carol_Debt_After) + + assert.equal(whale_Coll_Before, whale_Coll_After) + assert.equal(alice_Coll_Before, alice_Coll_After) + assert.equal(bob_Coll_Before, bob_Coll_After) + assert.equal(carol_Coll_Before, carol_Coll_After) + + assert.equal(whale_ICR_Before, whale_ICR_After) + assert.equal(alice_ICR_Before, alice_ICR_After) + assert.equal(bob_ICR_Before, bob_ICR_After) + assert.equal(carol_ICR_Before, carol_ICR_After) + }) + + it("withdrawFromSP(): succeeds when amount is 0 and system has an undercollateralized trove", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(100, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + + await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: A }) + + const A_initialDeposit = ((await stabilityPool.deposits(A))[0]).toString() + assert.equal(A_initialDeposit, dec(100, 18)) + + // defaulters opens trove + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) + + // ETH drops, defaulters are in liquidation range + await priceFeed.setPrice(dec(105, 18)) + const price = await priceFeed.getPrice() + assert.isTrue(await th.ICRbetween100and110(defaulter_1, troveManager, price)) + + await th.fastForwardTime(timeValues.MINUTES_IN_ONE_WEEK, web3.currentProvider) + + // Liquidate d1 + await troveManager.liquidate(defaulter_1) + assert.isFalse(await sortedTroves.contains(defaulter_1)) + + // Check d2 is undercollateralized + assert.isTrue(await th.ICRbetween100and110(defaulter_2, troveManager, price)) + assert.isTrue(await sortedTroves.contains(defaulter_2)) + + const A_ETHBalBefore = toBN(await web3.eth.getBalance(A)) + const A_LQTYBalBefore = await lqtyToken.balanceOf(A) + + // Check Alice has gains to withdraw + const A_pendingETHGain = await stabilityPool.getDepositorETHGain(A) + const A_pendingLQTYGain = await stabilityPool.getDepositorLQTYGain(A) + assert.isTrue(A_pendingETHGain.gt(toBN('0'))) + assert.isTrue(A_pendingLQTYGain.gt(toBN('0'))) + + // Check withdrawal of 0 succeeds + const tx = await stabilityPool.withdrawFromSP(0, { from: A, gasPrice: 0 }) + assert.isTrue(tx.receipt.status) + + const A_ETHBalAfter = toBN(await web3.eth.getBalance(A)) + + const A_LQTYBalAfter = await lqtyToken.balanceOf(A) + const A_LQTYBalDiff = A_LQTYBalAfter.sub(A_LQTYBalBefore) + + // Check A's ETH and LQTY balances have increased correctly + assert.isTrue(A_ETHBalAfter.sub(A_ETHBalBefore).eq(A_pendingETHGain)) + assert.isAtMost(th.getDifference(A_LQTYBalDiff, A_pendingLQTYGain), 1000) + }) + + it("withdrawFromSP(): withdrawing 0 LUSD doesn't alter the caller's deposit or the total LUSD in the Stability Pool", async () => { + // --- SETUP --- + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C open troves and make Stability Pool deposits + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) + + // A, B, C provides 100, 50, 30 LUSD to SP + await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: alice }) + await stabilityPool.provideToSP(dec(50, 18), frontEnd_1, { from: bob }) + await stabilityPool.provideToSP(dec(30, 18), frontEnd_1, { from: carol }) + + const bob_Deposit_Before = (await stabilityPool.getCompoundedLUSDDeposit(bob)).toString() + const LUSDinSP_Before = (await stabilityPool.getTotalLUSDDeposits()).toString() + + assert.equal(LUSDinSP_Before, dec(180, 18)) + + // Bob withdraws 0 LUSD from the Stability Pool + await stabilityPool.withdrawFromSP(0, { from: bob }) + + // check Bob's deposit and total LUSD in Stability Pool has not changed + const bob_Deposit_After = (await stabilityPool.getCompoundedLUSDDeposit(bob)).toString() + const LUSDinSP_After = (await stabilityPool.getTotalLUSDDeposits()).toString() + + assert.equal(bob_Deposit_Before, bob_Deposit_After) + assert.equal(LUSDinSP_Before, LUSDinSP_After) + }) + + it("withdrawFromSP(): withdrawing 0 ETH Gain does not alter the caller's ETH balance, their trove collateral, or the ETH in the Stability Pool", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C open troves and make Stability Pool deposits + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) + + // Would-be defaulter open trove + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // Price drops + await priceFeed.setPrice(dec(105, 18)) + + assert.isFalse(await th.checkRecoveryMode(contracts)) + + // Defaulter 1 liquidated, full offset + await troveManager.liquidate(defaulter_1) + + // Dennis opens trove and deposits to Stability Pool + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: dennis } }) + await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: dennis }) + + // Check Dennis has 0 ETHGain + const dennis_ETHGain = (await stabilityPool.getDepositorETHGain(dennis)).toString() + assert.equal(dennis_ETHGain, '0') + + const dennis_ETHBalance_Before = (web3.eth.getBalance(dennis)).toString() + const dennis_Collateral_Before = ((await troveManager.Troves(dennis))[1]).toString() + const ETHinSP_Before = (await stabilityPool.getETH()).toString() + + await priceFeed.setPrice(dec(200, 18)) + + // Dennis withdraws his full deposit and ETHGain to his account + await stabilityPool.withdrawFromSP(dec(100, 18), { from: dennis, gasPrice: 0 }) + + // Check withdrawal does not alter Dennis' ETH balance or his trove's collateral + const dennis_ETHBalance_After = (web3.eth.getBalance(dennis)).toString() + const dennis_Collateral_After = ((await troveManager.Troves(dennis))[1]).toString() + const ETHinSP_After = (await stabilityPool.getETH()).toString() + + assert.equal(dennis_ETHBalance_Before, dennis_ETHBalance_After) + assert.equal(dennis_Collateral_Before, dennis_Collateral_After) + + // Check withdrawal has not altered the ETH in the Stability Pool + assert.equal(ETHinSP_Before, ETHinSP_After) + }) + + it("withdrawFromSP(): Request to withdraw > caller's deposit only withdraws the caller's compounded deposit", async () => { + // --- SETUP --- + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C open troves and make Stability Pool deposits + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) + + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // A, B, C provide LUSD to SP + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: alice }) + await stabilityPool.provideToSP(dec(20000, 18), frontEnd_1, { from: bob }) + await stabilityPool.provideToSP(dec(30000, 18), frontEnd_1, { from: carol }) + + // Price drops + await priceFeed.setPrice(dec(105, 18)) + + // Liquidate defaulter 1 + await troveManager.liquidate(defaulter_1) + + const alice_LUSD_Balance_Before = await lusdToken.balanceOf(alice) + const bob_LUSD_Balance_Before = await lusdToken.balanceOf(bob) + + const alice_Deposit_Before = await stabilityPool.getCompoundedLUSDDeposit(alice) + const bob_Deposit_Before = await stabilityPool.getCompoundedLUSDDeposit(bob) + + const LUSDinSP_Before = await stabilityPool.getTotalLUSDDeposits() + + await priceFeed.setPrice(dec(200, 18)) + + // Bob attempts to withdraws 1 wei more than his compounded deposit from the Stability Pool + await stabilityPool.withdrawFromSP(bob_Deposit_Before.add(toBN(1)), { from: bob }) + + // Check Bob's LUSD balance has risen by only the value of his compounded deposit + const bob_expectedLUSDBalance = (bob_LUSD_Balance_Before.add(bob_Deposit_Before)).toString() + const bob_LUSD_Balance_After = (await lusdToken.balanceOf(bob)).toString() + assert.equal(bob_LUSD_Balance_After, bob_expectedLUSDBalance) + + // Alice attempts to withdraws 2309842309.000000000000000000 LUSD from the Stability Pool + await stabilityPool.withdrawFromSP('2309842309000000000000000000', { from: alice }) + + // Check Alice's LUSD balance has risen by only the value of her compounded deposit + const alice_expectedLUSDBalance = (alice_LUSD_Balance_Before.add(alice_Deposit_Before)).toString() + const alice_LUSD_Balance_After = (await lusdToken.balanceOf(alice)).toString() + assert.equal(alice_LUSD_Balance_After, alice_expectedLUSDBalance) + + // Check LUSD in Stability Pool has been reduced by only Alice's compounded deposit and Bob's compounded deposit + const expectedLUSDinSP = (LUSDinSP_Before.sub(alice_Deposit_Before).sub(bob_Deposit_Before)).toString() + const LUSDinSP_After = (await stabilityPool.getTotalLUSDDeposits()).toString() + assert.equal(LUSDinSP_After, expectedLUSDinSP) + }) + + it("withdrawFromSP(): Request to withdraw 2^256-1 LUSD only withdraws the caller's compounded deposit", async () => { + // --- SETUP --- + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C open troves + // A, B, C open troves + // A, B, C open troves + // A, B, C open troves + // A, B, C open troves + // A, B, C open troves + // A, B, C open troves + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) + + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // A, B, C provides 100, 50, 30 LUSD to SP + await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: alice }) + await stabilityPool.provideToSP(dec(50, 18), frontEnd_1, { from: bob }) + await stabilityPool.provideToSP(dec(30, 18), frontEnd_1, { from: carol }) + + // Price drops + await priceFeed.setPrice(dec(100, 18)) + + // Liquidate defaulter 1 + await troveManager.liquidate(defaulter_1) + + const bob_LUSD_Balance_Before = await lusdToken.balanceOf(bob) + + const bob_Deposit_Before = await stabilityPool.getCompoundedLUSDDeposit(bob) + + const LUSDinSP_Before = await stabilityPool.getTotalLUSDDeposits() + + const maxBytes32 = web3.utils.toBN("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") + + // Price drops + await priceFeed.setPrice(dec(200, 18)) + + // Bob attempts to withdraws maxBytes32 LUSD from the Stability Pool + await stabilityPool.withdrawFromSP(maxBytes32, { from: bob }) + + // Check Bob's LUSD balance has risen by only the value of his compounded deposit + const bob_expectedLUSDBalance = (bob_LUSD_Balance_Before.add(bob_Deposit_Before)).toString() + const bob_LUSD_Balance_After = (await lusdToken.balanceOf(bob)).toString() + assert.equal(bob_LUSD_Balance_After, bob_expectedLUSDBalance) + + // Check LUSD in Stability Pool has been reduced by only Bob's compounded deposit + const expectedLUSDinSP = (LUSDinSP_Before.sub(bob_Deposit_Before)).toString() + const LUSDinSP_After = (await stabilityPool.getTotalLUSDDeposits()).toString() + assert.equal(LUSDinSP_After, expectedLUSDinSP) + }) + + it("withdrawFromSP(): caller can withdraw full deposit and ETH gain during Recovery Mode", async () => { + // --- SETUP --- + + // Price doubles + await priceFeed.setPrice(dec(400, 18)) + await openTrove({ extraLUSDAmount: toBN(dec(1000000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale } }) + // Price halves + await priceFeed.setPrice(dec(200, 18)) + + // A, B, C open troves and make Stability Pool deposits + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(4, 18)), extraParams: { from: alice } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(4, 18)), extraParams: { from: bob } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(4, 18)), extraParams: { from: carol } }) + + await borrowerOperations.openTrove(th._100pct, await getOpenTroveLUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }) + + // A, B, C provides 10000, 5000, 3000 LUSD to SP + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: alice }) + await stabilityPool.provideToSP(dec(5000, 18), frontEnd_1, { from: bob }) + await stabilityPool.provideToSP(dec(3000, 18), frontEnd_1, { from: carol }) + + // Price drops + await priceFeed.setPrice(dec(105, 18)) + const price = await priceFeed.getPrice() + + assert.isTrue(await th.checkRecoveryMode(contracts)) + + // Liquidate defaulter 1 + await troveManager.liquidate(defaulter_1) + assert.isFalse(await sortedTroves.contains(defaulter_1)) + + const alice_LUSD_Balance_Before = await lusdToken.balanceOf(alice) + const bob_LUSD_Balance_Before = await lusdToken.balanceOf(bob) + const carol_LUSD_Balance_Before = await lusdToken.balanceOf(carol) + + const alice_ETH_Balance_Before = web3.utils.toBN(await web3.eth.getBalance(alice)) + const bob_ETH_Balance_Before = web3.utils.toBN(await web3.eth.getBalance(bob)) + const carol_ETH_Balance_Before = web3.utils.toBN(await web3.eth.getBalance(carol)) + + const alice_Deposit_Before = await stabilityPool.getCompoundedLUSDDeposit(alice) + const bob_Deposit_Before = await stabilityPool.getCompoundedLUSDDeposit(bob) + const carol_Deposit_Before = await stabilityPool.getCompoundedLUSDDeposit(carol) + + const alice_ETHGain_Before = await stabilityPool.getDepositorETHGain(alice) + const bob_ETHGain_Before = await stabilityPool.getDepositorETHGain(bob) + const carol_ETHGain_Before = await stabilityPool.getDepositorETHGain(carol) + + const LUSDinSP_Before = await stabilityPool.getTotalLUSDDeposits() + + // Price rises + await priceFeed.setPrice(dec(220, 18)) + + assert.isTrue(await th.checkRecoveryMode(contracts)) + + // A, B, C withdraw their full deposits from the Stability Pool + await stabilityPool.withdrawFromSP(dec(10000, 18), { from: alice, gasPrice: 0 }) + await stabilityPool.withdrawFromSP(dec(5000, 18), { from: bob, gasPrice: 0 }) + await stabilityPool.withdrawFromSP(dec(3000, 18), { from: carol, gasPrice: 0 }) + + // Check LUSD balances of A, B, C have risen by the value of their compounded deposits, respectively + const alice_expectedLUSDBalance = (alice_LUSD_Balance_Before.add(alice_Deposit_Before)).toString() + + const bob_expectedLUSDBalance = (bob_LUSD_Balance_Before.add(bob_Deposit_Before)).toString() + const carol_expectedLUSDBalance = (carol_LUSD_Balance_Before.add(carol_Deposit_Before)).toString() + + const alice_LUSD_Balance_After = (await lusdToken.balanceOf(alice)).toString() + + const bob_LUSD_Balance_After = (await lusdToken.balanceOf(bob)).toString() + const carol_LUSD_Balance_After = (await lusdToken.balanceOf(carol)).toString() + + assert.equal(alice_LUSD_Balance_After, alice_expectedLUSDBalance) + assert.equal(bob_LUSD_Balance_After, bob_expectedLUSDBalance) + assert.equal(carol_LUSD_Balance_After, carol_expectedLUSDBalance) + + // Check ETH balances of A, B, C have increased by the value of their ETH gain from liquidations, respectively + const alice_expectedETHBalance = (alice_ETH_Balance_Before.add(alice_ETHGain_Before)).toString() + const bob_expectedETHBalance = (bob_ETH_Balance_Before.add(bob_ETHGain_Before)).toString() + const carol_expectedETHBalance = (carol_ETH_Balance_Before.add(carol_ETHGain_Before)).toString() + + const alice_ETHBalance_After = (await web3.eth.getBalance(alice)).toString() + const bob_ETHBalance_After = (await web3.eth.getBalance(bob)).toString() + const carol_ETHBalance_After = (await web3.eth.getBalance(carol)).toString() + + assert.equal(alice_expectedETHBalance, alice_ETHBalance_After) + assert.equal(bob_expectedETHBalance, bob_ETHBalance_After) + assert.equal(carol_expectedETHBalance, carol_ETHBalance_After) + + // Check LUSD in Stability Pool has been reduced by A, B and C's compounded deposit + const expectedLUSDinSP = (LUSDinSP_Before + .sub(alice_Deposit_Before) + .sub(bob_Deposit_Before) + .sub(carol_Deposit_Before)) + .toString() + const LUSDinSP_After = (await stabilityPool.getTotalLUSDDeposits()).toString() + assert.equal(LUSDinSP_After, expectedLUSDinSP) + + // Check ETH in SP has reduced to zero + const ETHinSP_After = (await stabilityPool.getETH()).toString() + assert.isAtMost(th.getDifference(ETHinSP_After, '0'), 100000) + }) + + it("getDepositorETHGain(): depositor does not earn further ETH gains from liquidations while their compounded deposit == 0: ", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(1, 24)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C open troves + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) + + // defaulters open troves + await openTrove({ extraLUSDAmount: toBN(dec(15000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_3 } }) + + // A, B, provide 10000, 5000 LUSD to SP + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: alice }) + await stabilityPool.provideToSP(dec(5000, 18), frontEnd_1, { from: bob }) + + //price drops + await priceFeed.setPrice(dec(105, 18)) + + // Liquidate defaulter 1. Empties the Pool + await troveManager.liquidate(defaulter_1) + assert.isFalse(await sortedTroves.contains(defaulter_1)) + + const LUSDinSP = (await stabilityPool.getTotalLUSDDeposits()).toString() + assert.equal(LUSDinSP, '0') + + // Check Stability deposits have been fully cancelled with debt, and are now all zero + const alice_Deposit = (await stabilityPool.getCompoundedLUSDDeposit(alice)).toString() + const bob_Deposit = (await stabilityPool.getCompoundedLUSDDeposit(bob)).toString() + + assert.equal(alice_Deposit, '0') + assert.equal(bob_Deposit, '0') + + // Get ETH gain for A and B + const alice_ETHGain_1 = (await stabilityPool.getDepositorETHGain(alice)).toString() + const bob_ETHGain_1 = (await stabilityPool.getDepositorETHGain(bob)).toString() + + // Whale deposits 10000 LUSD to Stability Pool + await stabilityPool.provideToSP(dec(1, 24), frontEnd_1, { from: whale }) + + // Liquidation 2 + await troveManager.liquidate(defaulter_2) + assert.isFalse(await sortedTroves.contains(defaulter_2)) + + // Check Alice and Bob have not received ETH gain from liquidation 2 while their deposit was 0 + const alice_ETHGain_2 = (await stabilityPool.getDepositorETHGain(alice)).toString() + const bob_ETHGain_2 = (await stabilityPool.getDepositorETHGain(bob)).toString() + + assert.equal(alice_ETHGain_1, alice_ETHGain_2) + assert.equal(bob_ETHGain_1, bob_ETHGain_2) + + // Liquidation 3 + await troveManager.liquidate(defaulter_3) + assert.isFalse(await sortedTroves.contains(defaulter_3)) + + // Check Alice and Bob have not received ETH gain from liquidation 3 while their deposit was 0 + const alice_ETHGain_3 = (await stabilityPool.getDepositorETHGain(alice)).toString() + const bob_ETHGain_3 = (await stabilityPool.getDepositorETHGain(bob)).toString() + + assert.equal(alice_ETHGain_1, alice_ETHGain_3) + assert.equal(bob_ETHGain_1, bob_ETHGain_3) + }) + + // --- LQTY functionality --- + it("withdrawFromSP(): triggers LQTY reward event - increases the sum G", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(1, 24)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C open troves + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + + // A and B provide to SP + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(10000, 18), ZERO_ADDRESS, { from: B }) + + const G_Before = await stabilityPool.epochToScaleToG(0, 0) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // A withdraws from SP + await stabilityPool.withdrawFromSP(dec(5000, 18), { from: A }) + + const G_1 = await stabilityPool.epochToScaleToG(0, 0) + + // Expect G has increased from the LQTY reward event triggered + assert.isTrue(G_1.gt(G_Before)) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // A withdraws from SP + await stabilityPool.withdrawFromSP(dec(5000, 18), { from: B }) + + const G_2 = await stabilityPool.epochToScaleToG(0, 0) + + // Expect G has increased from the LQTY reward event triggered + assert.isTrue(G_2.gt(G_1)) + }) + + it("withdrawFromSP(), partial withdrawal: doesn't change the front end tag", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // whale transfer to troves D and E + await lusdToken.transfer(D, dec(100, 18), { from: whale }) + await lusdToken.transfer(E, dec(200, 18), { from: whale }) + + // A, B, C open troves + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + + // A, B, C, D, E provide to SP + await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: B }) + await stabilityPool.provideToSP(dec(30, 18), ZERO_ADDRESS, { from: C }) + await stabilityPool.provideToSP(dec(40, 18), frontEnd_1, { from: D }) + await stabilityPool.provideToSP(dec(50, 18), ZERO_ADDRESS, { from: E }) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // A, B, C, D, E withdraw, from different front ends + await stabilityPool.withdrawFromSP(dec(5, 18), { from: A }) + await stabilityPool.withdrawFromSP(dec(10, 18), { from: B }) + await stabilityPool.withdrawFromSP(dec(15, 18), { from: C }) + await stabilityPool.withdrawFromSP(dec(20, 18), { from: D }) + await stabilityPool.withdrawFromSP(dec(25, 18), { from: E }) + + const frontEndTag_A = (await stabilityPool.deposits(A))[1] + const frontEndTag_B = (await stabilityPool.deposits(B))[1] + const frontEndTag_C = (await stabilityPool.deposits(C))[1] + const frontEndTag_D = (await stabilityPool.deposits(D))[1] + const frontEndTag_E = (await stabilityPool.deposits(E))[1] + + // Check deposits are still tagged with their original front end + assert.equal(frontEndTag_A, frontEnd_1) + assert.equal(frontEndTag_B, frontEnd_2) + assert.equal(frontEndTag_C, ZERO_ADDRESS) + assert.equal(frontEndTag_D, frontEnd_1) + assert.equal(frontEndTag_E, ZERO_ADDRESS) + }) + + it("withdrawFromSP(), partial withdrawal: depositor receives LQTY rewards", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C open troves + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + + // A, B, C, provide to SP + await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: B }) + await stabilityPool.provideToSP(dec(30, 18), ZERO_ADDRESS, { from: C }) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // Get A, B, C LQTY balance before + const A_LQTYBalance_Before = await lqtyToken.balanceOf(A) + const B_LQTYBalance_Before = await lqtyToken.balanceOf(B) + const C_LQTYBalance_Before = await lqtyToken.balanceOf(C) + + // A, B, C withdraw + await stabilityPool.withdrawFromSP(dec(1, 18), { from: A }) + await stabilityPool.withdrawFromSP(dec(2, 18), { from: B }) + await stabilityPool.withdrawFromSP(dec(3, 18), { from: C }) + + // Get LQTY balance after + const A_LQTYBalance_After = await lqtyToken.balanceOf(A) + const B_LQTYBalance_After = await lqtyToken.balanceOf(B) + const C_LQTYBalance_After = await lqtyToken.balanceOf(C) + + // Check LQTY Balance of A, B, C has increased + assert.isTrue(A_LQTYBalance_After.gt(A_LQTYBalance_Before)) + assert.isTrue(B_LQTYBalance_After.gt(B_LQTYBalance_Before)) + assert.isTrue(C_LQTYBalance_After.gt(C_LQTYBalance_Before)) + }) + + it("withdrawFromSP(), partial withdrawal: tagged front end receives LQTY rewards", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C open troves + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + + // A, B, C, provide to SP + await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: B }) + await stabilityPool.provideToSP(dec(30, 18), frontEnd_3, { from: C }) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // Get front ends' LQTY balance before + const F1_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_1) + const F2_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_2) + const F3_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_3) + + // A, B, C withdraw + await stabilityPool.withdrawFromSP(dec(1, 18), { from: A }) + await stabilityPool.withdrawFromSP(dec(2, 18), { from: B }) + await stabilityPool.withdrawFromSP(dec(3, 18), { from: C }) + + // Get front ends' LQTY balance after + const F1_LQTYBalance_After = await lqtyToken.balanceOf(A) + const F2_LQTYBalance_After = await lqtyToken.balanceOf(B) + const F3_LQTYBalance_After = await lqtyToken.balanceOf(C) + + // Check LQTY Balance of front ends has increased + assert.isTrue(F1_LQTYBalance_After.gt(F1_LQTYBalance_Before)) + assert.isTrue(F2_LQTYBalance_After.gt(F2_LQTYBalance_Before)) + assert.isTrue(F3_LQTYBalance_After.gt(F3_LQTYBalance_Before)) + }) + + it("withdrawFromSP(), partial withdrawal: tagged front end's stake decreases", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C, D, E, F open troves + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: E } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: F } }) + + // A, B, C, D, E, F provide to SP + await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: B }) + await stabilityPool.provideToSP(dec(30, 18), frontEnd_3, { from: C }) + await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: D }) + await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: E }) + await stabilityPool.provideToSP(dec(30, 18), frontEnd_3, { from: F }) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // Get front ends' stake before + const F1_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_1) + const F2_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_2) + const F3_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_3) + + // A, B, C withdraw + await stabilityPool.withdrawFromSP(dec(1, 18), { from: A }) + await stabilityPool.withdrawFromSP(dec(2, 18), { from: B }) + await stabilityPool.withdrawFromSP(dec(3, 18), { from: C }) + + // Get front ends' stakes after + const F1_Stake_After = await stabilityPool.frontEndStakes(frontEnd_1) + const F2_Stake_After = await stabilityPool.frontEndStakes(frontEnd_2) + const F3_Stake_After = await stabilityPool.frontEndStakes(frontEnd_3) + + // Check front ends' stakes have decreased + assert.isTrue(F1_Stake_After.lt(F1_Stake_Before)) + assert.isTrue(F2_Stake_After.lt(F2_Stake_Before)) + assert.isTrue(F3_Stake_After.lt(F3_Stake_Before)) + }) + + it("withdrawFromSP(), partial withdrawal: tagged front end's snapshots update", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C, open troves + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(40000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(60000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + + // D opens trove + await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) + + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // --- SETUP --- + + const deposit_A = dec(10000, 18) + const deposit_B = dec(20000, 18) + const deposit_C = dec(30000, 18) + + // A, B, C make their initial deposits + await stabilityPool.provideToSP(deposit_A, frontEnd_1, { from: A }) + await stabilityPool.provideToSP(deposit_B, frontEnd_2, { from: B }) + await stabilityPool.provideToSP(deposit_C, frontEnd_3, { from: C }) + + // fastforward time then make an SP deposit, to make G > 0 + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + await stabilityPool.provideToSP(dec(1000, 18), ZERO_ADDRESS, { from: D }) + + // perform a liquidation to make 0 < P < 1, and S > 0 + await priceFeed.setPrice(dec(105, 18)) + assert.isFalse(await th.checkRecoveryMode(contracts)) + + await troveManager.liquidate(defaulter_1) + + const currentEpoch = await stabilityPool.currentEpoch() + const currentScale = await stabilityPool.currentScale() + + const S_Before = await stabilityPool.epochToScaleToSum(currentEpoch, currentScale) + const P_Before = await stabilityPool.P() + const G_Before = await stabilityPool.epochToScaleToG(currentEpoch, currentScale) + + // Confirm 0 < P < 1 + assert.isTrue(P_Before.gt(toBN('0')) && P_Before.lt(toBN(dec(1, 18)))) + // Confirm S, G are both > 0 + assert.isTrue(S_Before.gt(toBN('0'))) + assert.isTrue(G_Before.gt(toBN('0'))) + + // Get front ends' snapshots before + for (frontEnd of [frontEnd_1, frontEnd_2, frontEnd_3]) { + const snapshot = await stabilityPool.frontEndSnapshots(frontEnd) + + assert.equal(snapshot[0], '0') // S (should always be 0 for front ends, since S corresponds to ETH gain) + assert.equal(snapshot[1], dec(1, 18)) // P + assert.equal(snapshot[2], '0') // G + assert.equal(snapshot[3], '0') // scale + assert.equal(snapshot[4], '0') // epoch + } + + // --- TEST --- + + await priceFeed.setPrice(dec(200, 18)) + + // A, B, C top withdraw part of their deposits. Grab G at each stage, as it can increase a bit + // between topups, because some block.timestamp time passes (and LQTY is issued) between ops + const G1 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) + await stabilityPool.withdrawFromSP(dec(1, 18), { from: A }) + + const G2 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) + await stabilityPool.withdrawFromSP(dec(2, 18), { from: B }) + + const G3 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) + await stabilityPool.withdrawFromSP(dec(3, 18), { from: C }) + + const frontEnds = [frontEnd_1, frontEnd_2, frontEnd_3] + const G_Values = [G1, G2, G3] + + // Map frontEnds to the value of G at time the deposit was made + frontEndToG = th.zipToObject(frontEnds, G_Values) + + // Get front ends' snapshots after + for (const [frontEnd, G] of Object.entries(frontEndToG)) { + const snapshot = await stabilityPool.frontEndSnapshots(frontEnd) + + // Check snapshots are the expected values + assert.equal(snapshot[0], '0') // S (should always be 0 for front ends) + assert.isTrue(snapshot[1].eq(P_Before)) // P + assert.isTrue(snapshot[2].eq(G)) // G + assert.equal(snapshot[3], '0') // scale + assert.equal(snapshot[4], '0') // epoch + } + }) + + it("withdrawFromSP(), full withdrawal: removes deposit's front end tag", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // Whale transfers to A, B + await lusdToken.transfer(A, dec(10000, 18), { from: whale }) + await lusdToken.transfer(B, dec(20000, 18), { from: whale }) + + //C, D open troves + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + await openTrove({ extraLUSDAmount: toBN(dec(40000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) + + // A, B, C, D make their initial deposits + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(20000, 18), ZERO_ADDRESS, { from: B }) + await stabilityPool.provideToSP(dec(30000, 18), frontEnd_2, { from: C }) + await stabilityPool.provideToSP(dec(40000, 18), ZERO_ADDRESS, { from: D }) + + // Check deposits are tagged with correct front end + const A_tagBefore = await getFrontEndTag(stabilityPool, A) + const B_tagBefore = await getFrontEndTag(stabilityPool, B) + const C_tagBefore = await getFrontEndTag(stabilityPool, C) + const D_tagBefore = await getFrontEndTag(stabilityPool, D) + + assert.equal(A_tagBefore, frontEnd_1) + assert.equal(B_tagBefore, ZERO_ADDRESS) + assert.equal(C_tagBefore, frontEnd_2) + assert.equal(D_tagBefore, ZERO_ADDRESS) + + // All depositors make full withdrawal + await stabilityPool.withdrawFromSP(dec(10000, 18), { from: A }) + await stabilityPool.withdrawFromSP(dec(20000, 18), { from: B }) + await stabilityPool.withdrawFromSP(dec(30000, 18), { from: C }) + await stabilityPool.withdrawFromSP(dec(40000, 18), { from: D }) + + // Check all deposits now have no front end tag + const A_tagAfter = await getFrontEndTag(stabilityPool, A) + const B_tagAfter = await getFrontEndTag(stabilityPool, B) + const C_tagAfter = await getFrontEndTag(stabilityPool, C) + const D_tagAfter = await getFrontEndTag(stabilityPool, D) + + assert.equal(A_tagAfter, ZERO_ADDRESS) + assert.equal(B_tagAfter, ZERO_ADDRESS) + assert.equal(C_tagAfter, ZERO_ADDRESS) + assert.equal(D_tagAfter, ZERO_ADDRESS) + }) + + it("withdrawFromSP(), full withdrawal: zero's depositor's snapshots", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(1000000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // SETUP: Execute a series of operations to make G, S > 0 and P < 1 + + // E opens trove and makes a deposit + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: E } }) + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_3, { from: E }) + + // Fast-forward time and make a second deposit, to trigger LQTY reward and make G > 0 + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_3, { from: E }) + + // perform a liquidation to make 0 < P < 1, and S > 0 + await priceFeed.setPrice(dec(105, 18)) + assert.isFalse(await th.checkRecoveryMode(contracts)) + + await troveManager.liquidate(defaulter_1) + + const currentEpoch = await stabilityPool.currentEpoch() + const currentScale = await stabilityPool.currentScale() + + const S_Before = await stabilityPool.epochToScaleToSum(currentEpoch, currentScale) + const P_Before = await stabilityPool.P() + const G_Before = await stabilityPool.epochToScaleToG(currentEpoch, currentScale) + + // Confirm 0 < P < 1 + assert.isTrue(P_Before.gt(toBN('0')) && P_Before.lt(toBN(dec(1, 18)))) + // Confirm S, G are both > 0 + assert.isTrue(S_Before.gt(toBN('0'))) + assert.isTrue(G_Before.gt(toBN('0'))) + + // --- TEST --- + + // Whale transfers to A, B + await lusdToken.transfer(A, dec(10000, 18), { from: whale }) + await lusdToken.transfer(B, dec(20000, 18), { from: whale }) + + await priceFeed.setPrice(dec(200, 18)) + + // C, D open troves + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: C } }) + await openTrove({ extraLUSDAmount: toBN(dec(40000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: D } }) + + // A, B, C, D make their initial deposits + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(20000, 18), ZERO_ADDRESS, { from: B }) + await stabilityPool.provideToSP(dec(30000, 18), frontEnd_2, { from: C }) + await stabilityPool.provideToSP(dec(40000, 18), ZERO_ADDRESS, { from: D }) + + // Check deposits snapshots are non-zero + + for (depositor of [A, B, C, D]) { + const snapshot = await stabilityPool.depositSnapshots(depositor) + + const ZERO = toBN('0') + // Check S,P, G snapshots are non-zero + assert.isTrue(snapshot[0].eq(S_Before)) // S + assert.isTrue(snapshot[1].eq(P_Before)) // P + assert.isTrue(snapshot[2].gt(ZERO)) // GL increases a bit between each depositor op, so just check it is non-zero + assert.equal(snapshot[3], '0') // scale + assert.equal(snapshot[4], '0') // epoch + } + + // All depositors make full withdrawal + await stabilityPool.withdrawFromSP(dec(10000, 18), { from: A }) + await stabilityPool.withdrawFromSP(dec(20000, 18), { from: B }) + await stabilityPool.withdrawFromSP(dec(30000, 18), { from: C }) + await stabilityPool.withdrawFromSP(dec(40000, 18), { from: D }) + + // Check all depositors' snapshots have been zero'd + for (depositor of [A, B, C, D]) { + const snapshot = await stabilityPool.depositSnapshots(depositor) + + // Check S, P, G snapshots are now zero + assert.equal(snapshot[0], '0') // S + assert.equal(snapshot[1], '0') // P + assert.equal(snapshot[2], '0') // G + assert.equal(snapshot[3], '0') // scale + assert.equal(snapshot[4], '0') // epoch + } + }) + + it("withdrawFromSP(), full withdrawal that reduces front end stake to 0: zero’s the front end’s snapshots", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // SETUP: Execute a series of operations to make G, S > 0 and P < 1 + + // E opens trove and makes a deposit + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: E } }) + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_3, { from: E }) + + // Fast-forward time and make a second deposit, to trigger LQTY reward and make G > 0 + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_3, { from: E }) + + // perform a liquidation to make 0 < P < 1, and S > 0 + await priceFeed.setPrice(dec(105, 18)) + assert.isFalse(await th.checkRecoveryMode(contracts)) + + await troveManager.liquidate(defaulter_1) + + const currentEpoch = await stabilityPool.currentEpoch() + const currentScale = await stabilityPool.currentScale() + + const S_Before = await stabilityPool.epochToScaleToSum(currentEpoch, currentScale) + const P_Before = await stabilityPool.P() + const G_Before = await stabilityPool.epochToScaleToG(currentEpoch, currentScale) + + // Confirm 0 < P < 1 + assert.isTrue(P_Before.gt(toBN('0')) && P_Before.lt(toBN(dec(1, 18)))) + // Confirm S, G are both > 0 + assert.isTrue(S_Before.gt(toBN('0'))) + assert.isTrue(G_Before.gt(toBN('0'))) + + // --- TEST --- + + // A, B open troves + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(40000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + + // A, B, make their initial deposits + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(20000, 18), frontEnd_2, { from: B }) + + // Check frontend snapshots are non-zero + for (frontEnd of [frontEnd_1, frontEnd_2]) { + const snapshot = await stabilityPool.frontEndSnapshots(frontEnd) + + const ZERO = toBN('0') + // Check S,P, G snapshots are non-zero + assert.equal(snapshot[0], '0') // S (always zero for front-end) + assert.isTrue(snapshot[1].eq(P_Before)) // P + assert.isTrue(snapshot[2].gt(ZERO)) // GL increases a bit between each depositor op, so just check it is non-zero + assert.equal(snapshot[3], '0') // scale + assert.equal(snapshot[4], '0') // epoch + } + + await priceFeed.setPrice(dec(200, 18)) + + // All depositors make full withdrawal + await stabilityPool.withdrawFromSP(dec(10000, 18), { from: A }) + await stabilityPool.withdrawFromSP(dec(20000, 18), { from: B }) + + // Check all front ends' snapshots have been zero'd + for (frontEnd of [frontEnd_1, frontEnd_2]) { + const snapshot = await stabilityPool.frontEndSnapshots(frontEnd) + + // Check S, P, G snapshots are now zero + assert.equal(snapshot[0], '0') // S (always zero for front-end) + assert.equal(snapshot[1], '0') // P + assert.equal(snapshot[2], '0') // G + assert.equal(snapshot[3], '0') // scale + assert.equal(snapshot[4], '0') // epoch + } + }) + + it("withdrawFromSP(), reverts when initial deposit value is 0", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A opens trove and join the Stability Pool + await openTrove({ extraLUSDAmount: toBN(dec(10100, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: A }) + + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // SETUP: Execute a series of operations to trigger LQTY and ETH rewards for depositor A + + // Fast-forward time and make a second deposit, to trigger LQTY reward and make G > 0 + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: A }) + + // perform a liquidation to make 0 < P < 1, and S > 0 + await priceFeed.setPrice(dec(105, 18)) + assert.isFalse(await th.checkRecoveryMode(contracts)) + + await troveManager.liquidate(defaulter_1) + assert.isFalse(await sortedTroves.contains(defaulter_1)) + + await priceFeed.setPrice(dec(200, 18)) + + // A successfully withraws deposit and all gains + await stabilityPool.withdrawFromSP(dec(10100, 18), { from: A }) + + // Confirm A's recorded deposit is 0 + const A_deposit = (await stabilityPool.deposits(A))[0] // get initialValue property on deposit struct + assert.equal(A_deposit, '0') + + // --- TEST --- + const expectedRevertMessage = "StabilityPool: User must have a non-zero deposit" + + // Further withdrawal attempt from A + const withdrawalPromise_A = stabilityPool.withdrawFromSP(dec(10000, 18), { from: A }) + await th.assertRevert(withdrawalPromise_A, expectedRevertMessage) + + // Withdrawal attempt of a non-existent deposit, from C + const withdrawalPromise_C = stabilityPool.withdrawFromSP(dec(10000, 18), { from: C }) + await th.assertRevert(withdrawalPromise_C, expectedRevertMessage) + }) + + // --- withdrawETHGainToTrove --- + + it("withdrawETHGainToTrove(): reverts when user has no active deposit", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) + + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: alice }) + + const alice_initialDeposit = ((await stabilityPool.deposits(alice))[0]).toString() + const bob_initialDeposit = ((await stabilityPool.deposits(bob))[0]).toString() + + assert.equal(alice_initialDeposit, dec(10000, 18)) + assert.equal(bob_initialDeposit, '0') + + // Defaulter opens a trove, price drops, defaulter gets liquidated + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + await priceFeed.setPrice(dec(105, 18)) + assert.isFalse(await th.checkRecoveryMode(contracts)) + await troveManager.liquidate(defaulter_1) + assert.isFalse(await sortedTroves.contains(defaulter_1)) + + const txAlice = await stabilityPool.withdrawETHGainToTrove(alice, alice, { from: alice }) + assert.isTrue(txAlice.receipt.status) + + const txPromise_B = stabilityPool.withdrawETHGainToTrove(bob, bob, { from: bob }) + await th.assertRevert(txPromise_B) + }) + + it("withdrawETHGainToTrove(): Applies LUSDLoss to user's deposit, and redirects ETH reward to user's Trove", async () => { + // --- SETUP --- + // Whale deposits 185000 LUSD in StabilityPool + await openTrove({ extraLUSDAmount: toBN(dec(1000000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + await stabilityPool.provideToSP(dec(185000, 18), frontEnd_1, { from: whale }) + + // Defaulter opens trove + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // --- TEST --- + + // Alice makes deposit #1: 15000 LUSD + await openTrove({ extraLUSDAmount: toBN(dec(15000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: alice } }) + await stabilityPool.provideToSP(dec(15000, 18), frontEnd_1, { from: alice }) + + // check Alice's Trove recorded ETH Before: + const aliceTrove_Before = await troveManager.Troves(alice) + const aliceTrove_ETH_Before = aliceTrove_Before[1] + assert.isTrue(aliceTrove_ETH_Before.gt(toBN('0'))) + + // price drops: defaulter's Trove falls below MCR, alice and whale Trove remain active + await priceFeed.setPrice(dec(105, 18)); + + // Defaulter's Trove is closed + const liquidationTx_1 = await troveManager.liquidate(defaulter_1, { from: owner }) + const [liquidatedDebt, liquidatedColl, ,] = th.getEmittedLiquidationValues(liquidationTx_1) + + const ETHGain_A = await stabilityPool.getDepositorETHGain(alice) + const compoundedDeposit_A = await stabilityPool.getCompoundedLUSDDeposit(alice) + + // Alice should receive rewards proportional to her deposit as share of total deposits + const expectedETHGain_A = liquidatedColl.mul(toBN(dec(15000, 18))).div(toBN(dec(200000, 18))) + const expectedLUSDLoss_A = liquidatedDebt.mul(toBN(dec(15000, 18))).div(toBN(dec(200000, 18))) + const expectedCompoundedDeposit_A = toBN(dec(15000, 18)).sub(expectedLUSDLoss_A) + + assert.isAtMost(th.getDifference(expectedCompoundedDeposit_A, compoundedDeposit_A), 100000) + + // Alice sends her ETH Gains to her Trove + await stabilityPool.withdrawETHGainToTrove(alice, alice, { from: alice }) + + // check Alice's LUSDLoss has been applied to her deposit expectedCompoundedDeposit_A + alice_deposit_afterDefault = ((await stabilityPool.deposits(alice))[0]) + assert.isAtMost(th.getDifference(alice_deposit_afterDefault, expectedCompoundedDeposit_A), 100000) + + // check alice's Trove recorded ETH has increased by the expected reward amount + const aliceTrove_After = await troveManager.Troves(alice) + const aliceTrove_ETH_After = aliceTrove_After[1] + + const Trove_ETH_Increase = (aliceTrove_ETH_After.sub(aliceTrove_ETH_Before)).toString() + + assert.equal(Trove_ETH_Increase, ETHGain_A) + }) + + it("withdrawETHGainToTrove(): reverts if it would leave trove with ICR < MCR", async () => { + // --- SETUP --- + // Whale deposits 1850 LUSD in StabilityPool + await openTrove({ extraLUSDAmount: toBN(dec(1000000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + await stabilityPool.provideToSP(dec(185000, 18), frontEnd_1, { from: whale }) + + // defaulter opened + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // --- TEST --- + + // Alice makes deposit #1: 15000 LUSD + await openTrove({ extraLUSDAmount: toBN(dec(15000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await stabilityPool.provideToSP(dec(15000, 18), frontEnd_1, { from: alice }) + + // check alice's Trove recorded ETH Before: + const aliceTrove_Before = await troveManager.Troves(alice) + const aliceTrove_ETH_Before = aliceTrove_Before[1] + assert.isTrue(aliceTrove_ETH_Before.gt(toBN('0'))) + + // price drops: defaulter's Trove falls below MCR + await priceFeed.setPrice(dec(10, 18)); + + // defaulter's Trove is closed. + await troveManager.liquidate(defaulter_1, { from: owner }) + + // Alice attempts to her ETH Gains to her Trove + await assertRevert(stabilityPool.withdrawETHGainToTrove(alice, alice, { from: alice }), + "BorrowerOps: An operation that would result in ICR < MCR is not permitted") + }) + + it("withdrawETHGainToTrove(): Subsequent deposit and withdrawal attempt from same account, with no intermediate liquidations, withdraws zero ETH", async () => { + // --- SETUP --- + // Whale deposits 1850 LUSD in StabilityPool + await openTrove({ extraLUSDAmount: toBN(dec(1000000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + await stabilityPool.provideToSP(dec(185000, 18), frontEnd_1, { from: whale }) + + // defaulter opened + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // --- TEST --- + + // Alice makes deposit #1: 15000 LUSD + await openTrove({ extraLUSDAmount: toBN(dec(15000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await stabilityPool.provideToSP(dec(15000, 18), frontEnd_1, { from: alice }) + + // check alice's Trove recorded ETH Before: + const aliceTrove_Before = await troveManager.Troves(alice) + const aliceTrove_ETH_Before = aliceTrove_Before[1] + assert.isTrue(aliceTrove_ETH_Before.gt(toBN('0'))) + + // price drops: defaulter's Trove falls below MCR + await priceFeed.setPrice(dec(105, 18)); + + // defaulter's Trove is closed. + await troveManager.liquidate(defaulter_1, { from: owner }) + + // price bounces back + await priceFeed.setPrice(dec(200, 18)); + + // Alice sends her ETH Gains to her Trove + await stabilityPool.withdrawETHGainToTrove(alice, alice, { from: alice }) + + assert.equal(await stabilityPool.getDepositorETHGain(alice), 0) + + const ETHinSP_Before = (await stabilityPool.getETH()).toString() + + // Alice attempts second withdrawal from SP to Trove - reverts, due to 0 ETH Gain + const txPromise_A = stabilityPool.withdrawETHGainToTrove(alice, alice, { from: alice }) + await th.assertRevert(txPromise_A) + + // Check ETH in pool does not change + const ETHinSP_1 = (await stabilityPool.getETH()).toString() + assert.equal(ETHinSP_Before, ETHinSP_1) + + await priceFeed.setPrice(dec(200, 18)); + + // Alice attempts third withdrawal (this time, from SP to her own account) + await stabilityPool.withdrawFromSP(dec(15000, 18), { from: alice }) + + // Check ETH in pool does not change + const ETHinSP_2 = (await stabilityPool.getETH()).toString() + assert.equal(ETHinSP_Before, ETHinSP_2) + }) + + it("withdrawETHGainToTrove(): decreases StabilityPool ETH and increases activePool ETH", async () => { + // --- SETUP --- + // Whale deposits 185000 LUSD in StabilityPool + await openTrove({ extraLUSDAmount: toBN(dec(1000000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + await stabilityPool.provideToSP(dec(185000, 18), frontEnd_1, { from: whale }) + + // defaulter opened + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // --- TEST --- + + // Alice makes deposit #1: 15000 LUSD + await openTrove({ extraLUSDAmount: toBN(dec(15000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await stabilityPool.provideToSP(dec(15000, 18), frontEnd_1, { from: alice }) + + // price drops: defaulter's Trove falls below MCR + await priceFeed.setPrice(dec(100, 18)); + + // defaulter's Trove is closed. + const liquidationTx = await troveManager.liquidate(defaulter_1) + const [liquidatedDebt, liquidatedColl, gasComp] = th.getEmittedLiquidationValues(liquidationTx) + + // Expect alice to be entitled to 15000/200000 of the liquidated coll + const aliceExpectedETHGain = liquidatedColl.mul(toBN(dec(15000, 18))).div(toBN(dec(200000, 18))) + const aliceETHGain = await stabilityPool.getDepositorETHGain(alice) + assert.isTrue(aliceExpectedETHGain.eq(aliceETHGain)) + + // price bounces back + await priceFeed.setPrice(dec(200, 18)); + + //check activePool and StabilityPool Ether before retrieval: + const active_ETH_Before = await activePool.getETH() + const stability_ETH_Before = await stabilityPool.getETH() + + // Alice retrieves redirects ETH gain to her Trove + await stabilityPool.withdrawETHGainToTrove(alice, alice, { from: alice }) + + const active_ETH_After = await activePool.getETH() + const stability_ETH_After = await stabilityPool.getETH() + + const active_ETH_Difference = (active_ETH_After.sub(active_ETH_Before)) // AP ETH should increase + const stability_ETH_Difference = (stability_ETH_Before.sub(stability_ETH_After)) // SP ETH should decrease + + // check Pool ETH values change by Alice's ETHGain, i.e 0.075 ETH + assert.isAtMost(th.getDifference(active_ETH_Difference, aliceETHGain), 10000) + assert.isAtMost(th.getDifference(stability_ETH_Difference, aliceETHGain), 10000) + }) + + it("withdrawETHGainToTrove(): All depositors are able to withdraw their ETH gain from the SP to their Trove", async () => { + // Whale opens trove + await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // Defaulter opens trove + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // 6 Accounts open troves and provide to SP + const depositors = [alice, bob, carol, dennis, erin, flyn] + for (account of depositors) { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: account } }) + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: account }) + } + + await priceFeed.setPrice(dec(105, 18)) + await troveManager.liquidate(defaulter_1) + + // price bounces back + await priceFeed.setPrice(dec(200, 18)); + + // All depositors attempt to withdraw + const tx1 = await stabilityPool.withdrawETHGainToTrove(alice, alice, { from: alice }) + assert.isTrue(tx1.receipt.status) + const tx2 = await stabilityPool.withdrawETHGainToTrove(bob, bob, { from: bob }) + assert.isTrue(tx1.receipt.status) + const tx3 = await stabilityPool.withdrawETHGainToTrove(carol, carol, { from: carol }) + assert.isTrue(tx1.receipt.status) + const tx4 = await stabilityPool.withdrawETHGainToTrove(dennis, dennis, { from: dennis }) + assert.isTrue(tx1.receipt.status) + const tx5 = await stabilityPool.withdrawETHGainToTrove(erin, erin, { from: erin }) + assert.isTrue(tx1.receipt.status) + const tx6 = await stabilityPool.withdrawETHGainToTrove(flyn, flyn, { from: flyn }) + assert.isTrue(tx1.receipt.status) + }) + + it("withdrawETHGainToTrove(): All depositors withdraw, each withdraw their correct ETH gain", async () => { + // Whale opens trove + await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // defaulter opened + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // 6 Accounts open troves and provide to SP + const depositors = [alice, bob, carol, dennis, erin, flyn] + for (account of depositors) { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: account } }) + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: account }) + } + const collBefore = (await troveManager.Troves(alice))[1] // all troves have same coll before + + await priceFeed.setPrice(dec(105, 18)) + const liquidationTx = await troveManager.liquidate(defaulter_1) + const [, liquidatedColl, ,] = th.getEmittedLiquidationValues(liquidationTx) + + + /* All depositors attempt to withdraw their ETH gain to their Trove. Each depositor + receives (liquidatedColl/ 6). + + Thus, expected new collateral for each depositor with 1 Ether in their trove originally, is + (1 + liquidatedColl/6) + */ + + const expectedCollGain= liquidatedColl.div(toBN('6')) + + await priceFeed.setPrice(dec(200, 18)) + + await stabilityPool.withdrawETHGainToTrove(alice, alice, { from: alice }) + const aliceCollAfter = (await troveManager.Troves(alice))[1] + assert.isAtMost(th.getDifference(aliceCollAfter.sub(collBefore), expectedCollGain), 10000) + + await stabilityPool.withdrawETHGainToTrove(bob, bob, { from: bob }) + const bobCollAfter = (await troveManager.Troves(bob))[1] + assert.isAtMost(th.getDifference(bobCollAfter.sub(collBefore), expectedCollGain), 10000) + + await stabilityPool.withdrawETHGainToTrove(carol, carol, { from: carol }) + const carolCollAfter = (await troveManager.Troves(carol))[1] + assert.isAtMost(th.getDifference(carolCollAfter.sub(collBefore), expectedCollGain), 10000) + + await stabilityPool.withdrawETHGainToTrove(dennis, dennis, { from: dennis }) + const dennisCollAfter = (await troveManager.Troves(dennis))[1] + assert.isAtMost(th.getDifference(dennisCollAfter.sub(collBefore), expectedCollGain), 10000) + + await stabilityPool.withdrawETHGainToTrove(erin, erin, { from: erin }) + const erinCollAfter = (await troveManager.Troves(erin))[1] + assert.isAtMost(th.getDifference(erinCollAfter.sub(collBefore), expectedCollGain), 10000) + + await stabilityPool.withdrawETHGainToTrove(flyn, flyn, { from: flyn }) + const flynCollAfter = (await troveManager.Troves(flyn))[1] + assert.isAtMost(th.getDifference(flynCollAfter.sub(collBefore), expectedCollGain), 10000) + }) + + it("withdrawETHGainToTrove(): caller can withdraw full deposit and ETH gain to their trove during Recovery Mode", async () => { + // --- SETUP --- + + // Defaulter opens + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // A, B, C open troves + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) + + // A, B, C provides 10000, 5000, 3000 LUSD to SP + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: alice }) + await stabilityPool.provideToSP(dec(5000, 18), frontEnd_1, { from: bob }) + await stabilityPool.provideToSP(dec(3000, 18), frontEnd_1, { from: carol }) + + assert.isFalse(await th.checkRecoveryMode(contracts)) + + // Price drops to 105, + await priceFeed.setPrice(dec(105, 18)) + const price = await priceFeed.getPrice() + + assert.isTrue(await th.checkRecoveryMode(contracts)) + + // Check defaulter 1 has ICR: 100% < ICR < 110%. + assert.isTrue(await th.ICRbetween100and110(defaulter_1, troveManager, price)) + + const alice_Collateral_Before = (await troveManager.Troves(alice))[1] + const bob_Collateral_Before = (await troveManager.Troves(bob))[1] + const carol_Collateral_Before = (await troveManager.Troves(carol))[1] + + // Liquidate defaulter 1 + assert.isTrue(await sortedTroves.contains(defaulter_1)) + await troveManager.liquidate(defaulter_1) + assert.isFalse(await sortedTroves.contains(defaulter_1)) + + const alice_ETHGain_Before = await stabilityPool.getDepositorETHGain(alice) + const bob_ETHGain_Before = await stabilityPool.getDepositorETHGain(bob) + const carol_ETHGain_Before = await stabilityPool.getDepositorETHGain(carol) + + // A, B, C withdraw their full ETH gain from the Stability Pool to their trove + await stabilityPool.withdrawETHGainToTrove(alice, alice, { from: alice }) + await stabilityPool.withdrawETHGainToTrove(bob, bob, { from: bob }) + await stabilityPool.withdrawETHGainToTrove(carol, carol, { from: carol }) + + // Check collateral of troves A, B, C has increased by the value of their ETH gain from liquidations, respectively + const alice_expectedCollateral = (alice_Collateral_Before.add(alice_ETHGain_Before)).toString() + const bob_expectedColalteral = (bob_Collateral_Before.add(bob_ETHGain_Before)).toString() + const carol_expectedCollateral = (carol_Collateral_Before.add(carol_ETHGain_Before)).toString() + + const alice_Collateral_After = (await troveManager.Troves(alice))[1] + const bob_Collateral_After = (await troveManager.Troves(bob))[1] + const carol_Collateral_After = (await troveManager.Troves(carol))[1] + + assert.equal(alice_expectedCollateral, alice_Collateral_After) + assert.equal(bob_expectedColalteral, bob_Collateral_After) + assert.equal(carol_expectedCollateral, carol_Collateral_After) + + // Check ETH in SP has reduced to zero + const ETHinSP_After = (await stabilityPool.getETH()).toString() + assert.isAtMost(th.getDifference(ETHinSP_After, '0'), 100000) + }) + + it("withdrawETHGainToTrove(): reverts if user has no trove", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C open troves + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) + + // Defaulter opens + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // A transfers LUSD to D + await lusdToken.transfer(dennis, dec(10000, 18), { from: alice }) + + // D deposits to Stability Pool + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: dennis }) + + //Price drops + await priceFeed.setPrice(dec(105, 18)) + + //Liquidate defaulter 1 + await troveManager.liquidate(defaulter_1) + assert.isFalse(await sortedTroves.contains(defaulter_1)) + + await priceFeed.setPrice(dec(200, 18)) + + // D attempts to withdraw his ETH gain to Trove + await th.assertRevert(stabilityPool.withdrawETHGainToTrove(dennis, dennis, { from: dennis }), "caller must have an active trove to withdraw ETHGain to") + }) + + it("withdrawETHGainToTrove(): triggers LQTY reward event - increases the sum G", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C open troves + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + + // A and B provide to SP + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(10000, 18), ZERO_ADDRESS, { from: B }) + + // Defaulter opens a trove, price drops, defaulter gets liquidated + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + await priceFeed.setPrice(dec(105, 18)) + assert.isFalse(await th.checkRecoveryMode(contracts)) + await troveManager.liquidate(defaulter_1) + assert.isFalse(await sortedTroves.contains(defaulter_1)) + + const G_Before = await stabilityPool.epochToScaleToG(0, 0) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + await priceFeed.setPrice(dec(200, 18)) + + // A withdraws from SP + await stabilityPool.withdrawFromSP(dec(50, 18), { from: A }) + + const G_1 = await stabilityPool.epochToScaleToG(0, 0) + + // Expect G has increased from the LQTY reward event triggered + assert.isTrue(G_1.gt(G_Before)) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // Check B has non-zero ETH gain + assert.isTrue((await stabilityPool.getDepositorETHGain(B)).gt(ZERO)) + + // B withdraws to trove + await stabilityPool.withdrawETHGainToTrove(B, B, { from: B }) + + const G_2 = await stabilityPool.epochToScaleToG(0, 0) + + // Expect G has increased from the LQTY reward event triggered + assert.isTrue(G_2.gt(G_1)) + }) + + it("withdrawETHGainToTrove(), partial withdrawal: doesn't change the front end tag", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C open troves + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + + // A, B, C, D, E provide to SP + await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(20000, 18), frontEnd_2, { from: B }) + await stabilityPool.provideToSP(dec(30000, 18), ZERO_ADDRESS, { from: C }) + + // Defaulter opens a trove, price drops, defaulter gets liquidated + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + await priceFeed.setPrice(dec(105, 18)) + assert.isFalse(await th.checkRecoveryMode(contracts)) + await troveManager.liquidate(defaulter_1) + assert.isFalse(await sortedTroves.contains(defaulter_1)) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // Check A, B, C have non-zero ETH gain + assert.isTrue((await stabilityPool.getDepositorETHGain(A)).gt(ZERO)) + assert.isTrue((await stabilityPool.getDepositorETHGain(B)).gt(ZERO)) + assert.isTrue((await stabilityPool.getDepositorETHGain(C)).gt(ZERO)) + + await priceFeed.setPrice(dec(200, 18)) + + // A, B, C withdraw to trove + await stabilityPool.withdrawETHGainToTrove(A, A, { from: A }) + await stabilityPool.withdrawETHGainToTrove(B, B, { from: B }) + await stabilityPool.withdrawETHGainToTrove(C, C, { from: C }) + + const frontEndTag_A = (await stabilityPool.deposits(A))[1] + const frontEndTag_B = (await stabilityPool.deposits(B))[1] + const frontEndTag_C = (await stabilityPool.deposits(C))[1] + + // Check deposits are still tagged with their original front end + assert.equal(frontEndTag_A, frontEnd_1) + assert.equal(frontEndTag_B, frontEnd_2) + assert.equal(frontEndTag_C, ZERO_ADDRESS) + }) + + it("withdrawETHGainToTrove(), eligible deposit: depositor receives LQTY rewards", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C open troves + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + + // A, B, C, provide to SP + await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(2000, 18), frontEnd_2, { from: B }) + await stabilityPool.provideToSP(dec(3000, 18), ZERO_ADDRESS, { from: C }) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // Defaulter opens a trove, price drops, defaulter gets liquidated + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + await priceFeed.setPrice(dec(105, 18)) + assert.isFalse(await th.checkRecoveryMode(contracts)) + await troveManager.liquidate(defaulter_1) + assert.isFalse(await sortedTroves.contains(defaulter_1)) + + // Get A, B, C LQTY balance before + const A_LQTYBalance_Before = await lqtyToken.balanceOf(A) + const B_LQTYBalance_Before = await lqtyToken.balanceOf(B) + const C_LQTYBalance_Before = await lqtyToken.balanceOf(C) + + // Check A, B, C have non-zero ETH gain + assert.isTrue((await stabilityPool.getDepositorETHGain(A)).gt(ZERO)) + assert.isTrue((await stabilityPool.getDepositorETHGain(B)).gt(ZERO)) + assert.isTrue((await stabilityPool.getDepositorETHGain(C)).gt(ZERO)) + + await priceFeed.setPrice(dec(200, 18)) + + // A, B, C withdraw to trove + await stabilityPool.withdrawETHGainToTrove(A, A, { from: A }) + await stabilityPool.withdrawETHGainToTrove(B, B, { from: B }) + await stabilityPool.withdrawETHGainToTrove(C, C, { from: C }) + + // Get LQTY balance after + const A_LQTYBalance_After = await lqtyToken.balanceOf(A) + const B_LQTYBalance_After = await lqtyToken.balanceOf(B) + const C_LQTYBalance_After = await lqtyToken.balanceOf(C) + + // Check LQTY Balance of A, B, C has increased + assert.isTrue(A_LQTYBalance_After.gt(A_LQTYBalance_Before)) + assert.isTrue(B_LQTYBalance_After.gt(B_LQTYBalance_Before)) + assert.isTrue(C_LQTYBalance_After.gt(C_LQTYBalance_Before)) + }) + + it("withdrawETHGainToTrove(), eligible deposit: tagged front end receives LQTY rewards", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C open troves + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + + // A, B, C, provide to SP + await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(2000, 18), frontEnd_2, { from: B }) + await stabilityPool.provideToSP(dec(3000, 18), frontEnd_3, { from: C }) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // Defaulter opens a trove, price drops, defaulter gets liquidated + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + await priceFeed.setPrice(dec(105, 18)) + assert.isFalse(await th.checkRecoveryMode(contracts)) + await troveManager.liquidate(defaulter_1) + assert.isFalse(await sortedTroves.contains(defaulter_1)) + + // Get front ends' LQTY balance before + const F1_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_1) + const F2_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_2) + const F3_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_3) + + await priceFeed.setPrice(dec(200, 18)) + + // Check A, B, C have non-zero ETH gain + assert.isTrue((await stabilityPool.getDepositorETHGain(A)).gt(ZERO)) + assert.isTrue((await stabilityPool.getDepositorETHGain(B)).gt(ZERO)) + assert.isTrue((await stabilityPool.getDepositorETHGain(C)).gt(ZERO)) + + // A, B, C withdraw + await stabilityPool.withdrawETHGainToTrove(A, A, { from: A }) + await stabilityPool.withdrawETHGainToTrove(B, B, { from: B }) + await stabilityPool.withdrawETHGainToTrove(C, C, { from: C }) + + // Get front ends' LQTY balance after + const F1_LQTYBalance_After = await lqtyToken.balanceOf(frontEnd_1) + const F2_LQTYBalance_After = await lqtyToken.balanceOf(frontEnd_2) + const F3_LQTYBalance_After = await lqtyToken.balanceOf(frontEnd_3) + + // Check LQTY Balance of front ends has increased + assert.isTrue(F1_LQTYBalance_After.gt(F1_LQTYBalance_Before)) + assert.isTrue(F2_LQTYBalance_After.gt(F2_LQTYBalance_Before)) + assert.isTrue(F3_LQTYBalance_After.gt(F3_LQTYBalance_Before)) + }) + + it("withdrawETHGainToTrove(), eligible deposit: tagged front end's stake decreases", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C, D, E, F open troves + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: E } }) + await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: F } }) + + // A, B, C, D, E, F provide to SP + await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(2000, 18), frontEnd_2, { from: B }) + await stabilityPool.provideToSP(dec(3000, 18), frontEnd_3, { from: C }) + await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: D }) + await stabilityPool.provideToSP(dec(2000, 18), frontEnd_2, { from: E }) + await stabilityPool.provideToSP(dec(3000, 18), frontEnd_3, { from: F }) + + // Defaulter opens a trove, price drops, defaulter gets liquidated + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + await priceFeed.setPrice(dec(105, 18)) + assert.isFalse(await th.checkRecoveryMode(contracts)) + await troveManager.liquidate(defaulter_1) + assert.isFalse(await sortedTroves.contains(defaulter_1)) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // Get front ends' stake before + const F1_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_1) + const F2_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_2) + const F3_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_3) + + await priceFeed.setPrice(dec(200, 18)) + + // Check A, B, C have non-zero ETH gain + assert.isTrue((await stabilityPool.getDepositorETHGain(A)).gt(ZERO)) + assert.isTrue((await stabilityPool.getDepositorETHGain(B)).gt(ZERO)) + assert.isTrue((await stabilityPool.getDepositorETHGain(C)).gt(ZERO)) + + // A, B, C withdraw to trove + await stabilityPool.withdrawETHGainToTrove(A, A, { from: A }) + await stabilityPool.withdrawETHGainToTrove(B, B, { from: B }) + await stabilityPool.withdrawETHGainToTrove(C, C, { from: C }) + + // Get front ends' stakes after + const F1_Stake_After = await stabilityPool.frontEndStakes(frontEnd_1) + const F2_Stake_After = await stabilityPool.frontEndStakes(frontEnd_2) + const F3_Stake_After = await stabilityPool.frontEndStakes(frontEnd_3) + + // Check front ends' stakes have decreased + assert.isTrue(F1_Stake_After.lt(F1_Stake_Before)) + assert.isTrue(F2_Stake_After.lt(F2_Stake_Before)) + assert.isTrue(F3_Stake_After.lt(F3_Stake_Before)) + }) + + it("withdrawETHGainToTrove(), eligible deposit: tagged front end's snapshots update", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C, open troves + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(40000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(60000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + + // D opens trove + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) + + await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) + + // --- SETUP --- + + const deposit_A = dec(100, 18) + const deposit_B = dec(200, 18) + const deposit_C = dec(300, 18) + + // A, B, C make their initial deposits + await stabilityPool.provideToSP(deposit_A, frontEnd_1, { from: A }) + await stabilityPool.provideToSP(deposit_B, frontEnd_2, { from: B }) + await stabilityPool.provideToSP(deposit_C, frontEnd_3, { from: C }) + + // fastforward time then make an SP deposit, to make G > 0 + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + await stabilityPool.provideToSP(dec(10000, 18), ZERO_ADDRESS, { from: D }) + + // perform a liquidation to make 0 < P < 1, and S > 0 + await priceFeed.setPrice(dec(105, 18)) + assert.isFalse(await th.checkRecoveryMode(contracts)) + + await troveManager.liquidate(defaulter_1) + + const currentEpoch = await stabilityPool.currentEpoch() + const currentScale = await stabilityPool.currentScale() + + const S_Before = await stabilityPool.epochToScaleToSum(currentEpoch, currentScale) + const P_Before = await stabilityPool.P() + const G_Before = await stabilityPool.epochToScaleToG(currentEpoch, currentScale) + + // Confirm 0 < P < 1 + assert.isTrue(P_Before.gt(toBN('0')) && P_Before.lt(toBN(dec(1, 18)))) + // Confirm S, G are both > 0 + assert.isTrue(S_Before.gt(toBN('0'))) + assert.isTrue(G_Before.gt(toBN('0'))) + + // Get front ends' snapshots before + for (frontEnd of [frontEnd_1, frontEnd_2, frontEnd_3]) { + const snapshot = await stabilityPool.frontEndSnapshots(frontEnd) + + assert.equal(snapshot[0], '0') // S (should always be 0 for front ends, since S corresponds to ETH gain) + assert.equal(snapshot[1], dec(1, 18)) // P + assert.equal(snapshot[2], '0') // G + assert.equal(snapshot[3], '0') // scale + assert.equal(snapshot[4], '0') // epoch + } + + // --- TEST --- + + // Check A, B, C have non-zero ETH gain + assert.isTrue((await stabilityPool.getDepositorETHGain(A)).gt(ZERO)) + assert.isTrue((await stabilityPool.getDepositorETHGain(B)).gt(ZERO)) + assert.isTrue((await stabilityPool.getDepositorETHGain(C)).gt(ZERO)) + + await priceFeed.setPrice(dec(200, 18)) + + // A, B, C withdraw ETH gain to troves. Grab G at each stage, as it can increase a bit + // between topups, because some block.timestamp time passes (and LQTY is issued) between ops + const G1 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) + await stabilityPool.withdrawETHGainToTrove(A, A, { from: A }) + + const G2 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) + await stabilityPool.withdrawETHGainToTrove(B, B, { from: B }) + + const G3 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) + await stabilityPool.withdrawETHGainToTrove(C, C, { from: C }) + + const frontEnds = [frontEnd_1, frontEnd_2, frontEnd_3] + const G_Values = [G1, G2, G3] + + // Map frontEnds to the value of G at time the deposit was made + frontEndToG = th.zipToObject(frontEnds, G_Values) + + // Get front ends' snapshots after + for (const [frontEnd, G] of Object.entries(frontEndToG)) { + const snapshot = await stabilityPool.frontEndSnapshots(frontEnd) + + // Check snapshots are the expected values + assert.equal(snapshot[0], '0') // S (should always be 0 for front ends) + assert.isTrue(snapshot[1].eq(P_Before)) // P + assert.isTrue(snapshot[2].eq(G)) // G + assert.equal(snapshot[3], '0') // scale + assert.equal(snapshot[4], '0') // epoch + } + }) + + it("withdrawETHGainToTrove(): reverts when depositor has no ETH gain", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // Whale transfers LUSD to A, B + await lusdToken.transfer(A, dec(10000, 18), { from: whale }) + await lusdToken.transfer(B, dec(20000, 18), { from: whale }) + + // C, D open troves + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + await openTrove({ extraLUSDAmount: toBN(dec(4000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) + + // A, B, C, D provide to SP + await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: A }) + await stabilityPool.provideToSP(dec(20, 18), ZERO_ADDRESS, { from: B }) + await stabilityPool.provideToSP(dec(30, 18), frontEnd_2, { from: C }) + await stabilityPool.provideToSP(dec(40, 18), ZERO_ADDRESS, { from: D }) + + // fastforward time, and E makes a deposit, creating LQTY rewards for all + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: E } }) + await stabilityPool.provideToSP(dec(3000, 18), ZERO_ADDRESS, { from: E }) + + // Confirm A, B, C have zero ETH gain + assert.equal(await stabilityPool.getDepositorETHGain(A), '0') + assert.equal(await stabilityPool.getDepositorETHGain(B), '0') + assert.equal(await stabilityPool.getDepositorETHGain(C), '0') + + // Check withdrawETHGainToTrove reverts for A, B, C + const txPromise_A = stabilityPool.withdrawETHGainToTrove(A, A, { from: A }) + const txPromise_B = stabilityPool.withdrawETHGainToTrove(B, B, { from: B }) + const txPromise_C = stabilityPool.withdrawETHGainToTrove(C, C, { from: C }) + const txPromise_D = stabilityPool.withdrawETHGainToTrove(D, D, { from: D }) + + await th.assertRevert(txPromise_A) + await th.assertRevert(txPromise_B) + await th.assertRevert(txPromise_C) + await th.assertRevert(txPromise_D) + }) + + it("registerFrontEnd(): registers the front end and chosen kickback rate", async () => { + const unregisteredFrontEnds = [A, B, C, D, E] + + for (const frontEnd of unregisteredFrontEnds) { + assert.isFalse((await stabilityPool.frontEnds(frontEnd))[1]) // check inactive + assert.equal((await stabilityPool.frontEnds(frontEnd))[0], '0') // check no chosen kickback rate + } + + await stabilityPool.registerFrontEnd(dec(1, 18), { from: A }) + await stabilityPool.registerFrontEnd('897789897897897', { from: B }) + await stabilityPool.registerFrontEnd('99990098', { from: C }) + await stabilityPool.registerFrontEnd('37', { from: D }) + await stabilityPool.registerFrontEnd('0', { from: E }) + + // Check front ends are registered as active, and have correct kickback rates + assert.isTrue((await stabilityPool.frontEnds(A))[1]) + assert.equal((await stabilityPool.frontEnds(A))[0], dec(1, 18)) + + assert.isTrue((await stabilityPool.frontEnds(B))[1]) + assert.equal((await stabilityPool.frontEnds(B))[0], '897789897897897') + + assert.isTrue((await stabilityPool.frontEnds(C))[1]) + assert.equal((await stabilityPool.frontEnds(C))[0], '99990098') + + assert.isTrue((await stabilityPool.frontEnds(D))[1]) + assert.equal((await stabilityPool.frontEnds(D))[0], '37') + + assert.isTrue((await stabilityPool.frontEnds(E))[1]) + assert.equal((await stabilityPool.frontEnds(E))[0], '0') + }) + + it("registerFrontEnd(): reverts if the front end is already registered", async () => { + + await stabilityPool.registerFrontEnd(dec(1, 18), { from: A }) + await stabilityPool.registerFrontEnd('897789897897897', { from: B }) + await stabilityPool.registerFrontEnd('99990098', { from: C }) + + const _2ndAttempt_A = stabilityPool.registerFrontEnd(dec(1, 18), { from: A }) + const _2ndAttempt_B = stabilityPool.registerFrontEnd('897789897897897', { from: B }) + const _2ndAttempt_C = stabilityPool.registerFrontEnd('99990098', { from: C }) + + await th.assertRevert(_2ndAttempt_A, "StabilityPool: must not already be a registered front end") + await th.assertRevert(_2ndAttempt_B, "StabilityPool: must not already be a registered front end") + await th.assertRevert(_2ndAttempt_C, "StabilityPool: must not already be a registered front end") + }) + + it("registerFrontEnd(): reverts if the kickback rate >1", async () => { + + const invalidKickbackTx_A = stabilityPool.registerFrontEnd(dec(1, 19), { from: A }) + const invalidKickbackTx_B = stabilityPool.registerFrontEnd('1000000000000000001', { from: A }) + const invalidKickbackTx_C = stabilityPool.registerFrontEnd(dec(23423, 45), { from: A }) + const invalidKickbackTx_D = stabilityPool.registerFrontEnd(maxBytes32, { from: A }) + + await th.assertRevert(invalidKickbackTx_A, "StabilityPool: Kickback rate must be in range [0,1]") + await th.assertRevert(invalidKickbackTx_B, "StabilityPool: Kickback rate must be in range [0,1]") + await th.assertRevert(invalidKickbackTx_C, "StabilityPool: Kickback rate must be in range [0,1]") + await th.assertRevert(invalidKickbackTx_D, "StabilityPool: Kickback rate must be in range [0,1]") + }) + + it("registerFrontEnd(): reverts if address has a non-zero deposit already", async () => { + // C, D, E open troves + await openTrove({ extraLUSDAmount: toBN(dec(10, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + await openTrove({ extraLUSDAmount: toBN(dec(10, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) + await openTrove({ extraLUSDAmount: toBN(dec(10, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: E } }) + + // C, E provides to SP + await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: C }) + await stabilityPool.provideToSP(dec(10, 18), ZERO_ADDRESS, { from: E }) + + const txPromise_C = stabilityPool.registerFrontEnd(dec(1, 18), { from: C }) + const txPromise_E = stabilityPool.registerFrontEnd(dec(1, 18), { from: E }) + await th.assertRevert(txPromise_C, "StabilityPool: User must have no deposit") + await th.assertRevert(txPromise_E, "StabilityPool: User must have no deposit") + + // D, with no deposit, successfully registers a front end + const txD = await stabilityPool.registerFrontEnd(dec(1, 18), { from: D }) + assert.isTrue(txD.receipt.status) + }) + }) +}) + +contract('Reset chain state', async accounts => { }) diff --git a/packages/contracts/test/B.Protocol/PriceFormulaTest.js b/packages/contracts/test/B.Protocol/PriceFormulaTest.js new file mode 100644 index 000000000..91ab1717f --- /dev/null +++ b/packages/contracts/test/B.Protocol/PriceFormulaTest.js @@ -0,0 +1,31 @@ +const Decimal = require("decimal.js"); +const { BNConverter } = require("../utils/BNConverter.js") +const testHelpers = require("../utils/testHelpers.js") +const PriceFormula = artifacts.require("./PriceFormula.sol") + +const th = testHelpers.TestHelper +const timeValues = testHelpers.TimeValues +const dec = th.dec +const toBN = th.toBN +const getDifference = th.getDifference + +contract('PriceFormula tests', async accounts => { + let priceFormula + + before(async () => { + priceFormula = await PriceFormula.new() + }) + + // numbers here were taken from the return value of mainnet contract + + it("check price 0", async () => { + const xQty = "1234567891" + const xBalance = "321851652450" + const yBalance = "219413622039" + const A = 200 + const ret = await priceFormula.getReturn(xQty, xBalance, yBalance, A); + const retAfterFee = ret.sub(ret.mul(toBN(4000000)).div(toBN(10**10))) + assert.equal(retAfterFee.toString(10), '1231543859') + }) +}) + From ab6a53a5a2a3d6845ca82dc1a2b98fdf44177aa1 Mon Sep 17 00:00:00 2001 From: yaron velner Date: Sat, 26 Jun 2021 19:08:16 +0300 Subject: [PATCH 02/90] lp token --- .../contracts/contracts/B.Protocol/BAMM.sol | 32 +- .../contracts/B.Protocol/LPToken.sol | 75 + .../contracts/test/B.Protocol/BAMMTest.js | 3549 +---------------- 3 files changed, 98 insertions(+), 3558 deletions(-) create mode 100644 packages/contracts/contracts/B.Protocol/LPToken.sol diff --git a/packages/contracts/contracts/B.Protocol/BAMM.sol b/packages/contracts/contracts/B.Protocol/BAMM.sol index bc87c2424..26c77a74d 100644 --- a/packages/contracts/contracts/B.Protocol/BAMM.sol +++ b/packages/contracts/contracts/B.Protocol/BAMM.sol @@ -3,7 +3,7 @@ pragma solidity 0.6.11; import "./../StabilityPool.sol"; -import "./crop.sol"; +import "./LPToken.sol"; import "./PriceFormula.sol"; import "./../Interfaces/IPriceFeed.sol"; import "./../Dependencies/IERC20.sol"; @@ -11,7 +11,7 @@ import "./../Dependencies/SafeMath.sol"; import "./../Dependencies/Ownable.sol"; import "./../Dependencies/AggregatorV3Interface.sol"; -contract BAMM is CropJoin, PriceFormula, Ownable { +contract BAMM is LPToken, PriceFormula, Ownable { using SafeMath for uint256; AggregatorV3Interface public immutable priceAggregator; @@ -32,7 +32,7 @@ contract BAMM is CropJoin, PriceFormula, Ownable { uint constant PRECISION = 1e18; constructor(address _priceAggregator, address payable _SP, address _LUSD, address _LQTY, uint _maxDiscount, address payable _feePool) public - CropJoin(address(new Dummy()), "B.AMM", address(new DummyGem()), _LQTY) { + LPToken(_LQTY) { priceAggregator = AggregatorV3Interface(_priceAggregator); SP = StabilityPool(_SP); LUSD = IERC20(_LUSD); @@ -105,8 +105,8 @@ contract BAMM is CropJoin, PriceFormula, Ownable { require(LUSD.transferFrom(msg.sender, address(this), lusdAmount), "deposit: transferFrom failed"); SP.provideToSP(lusdAmount, frontEndTag); - // update LQTY - join(msg.sender, newShare); + // update LP token + mint(msg.sender, newShare); } function withdraw(uint numShares) external { @@ -119,8 +119,8 @@ contract BAMM is CropJoin, PriceFormula, Ownable { // this withdraws lusd, lqty, and eth SP.withdrawFromSP(lusdAmount); - // update LQTY - exit(msg.sender, numShares); + // update LP token + burn(msg.sender, numShares); // send lusd and eth if(lusdAmount > 0) LUSD.transfer(msg.sender, lusdAmount); @@ -198,21 +198,3 @@ contract BAMM is CropJoin, PriceFormula, Ownable { receive() external payable {} } - -contract Dummy { - fallback() external payable {} -} - -contract DummyGem is Dummy { - function transfer(address, uint) external pure returns(bool) { - return true; - } - - function transferFrom(address, address, uint) external pure returns(bool) { - return true; - } - - function decimals() external pure returns(uint) { - return 18; - } -} \ No newline at end of file diff --git a/packages/contracts/contracts/B.Protocol/LPToken.sol b/packages/contracts/contracts/B.Protocol/LPToken.sol new file mode 100644 index 000000000..628937bc5 --- /dev/null +++ b/packages/contracts/contracts/B.Protocol/LPToken.sol @@ -0,0 +1,75 @@ +pragma solidity 0.6.11; + +import "./crop.sol"; + +contract LPToken is CropJoin { + string constant public name = "B.AMM LUSD-ETH"; + string constant public symbol = "LUSDETH"; + uint constant public decimals = 18; + mapping(address => mapping(address => uint)) allowance; + + event Transfer(address indexed _from, address indexed _to, uint256 _value); + event Approval(address indexed _owner, address indexed _spender, uint256 _value); + + constructor(address lqty) public + CropJoin(address(new Dummy()), "B.AMM", address(new DummyGem()), lqty) {} + + function mint(address to, uint value) internal { + join(to, value); + emit Transfer(address(0), to, value); + } + + function burn(address owner, uint value) internal { + exit(owner, value); + emit Transfer(owner, address(0), value); + } + + function totalSupply() public view returns (uint256) { + return total; + } + + function balanceOf(address owner) public view returns (uint256 balance) { + balance = stake[owner]; + } + + function transfer(address to, uint256 value) public returns (bool success) { + burn(msg.sender, value); + mint(to, value); + + emit Transfer(msg.sender, to, value); + success = true; + } + + function transferFrom(address from, address to, uint256 value) public returns (bool success) { + allowance[msg.sender][from] = sub(allowance[msg.sender][from], value); + + burn(from, value); + mint(to, value); + + emit Transfer(from, to, value); + success = true; + } + + function approve(address spender, uint256 value) public returns (bool success) { + allowance[msg.sender][spender] = value; + emit Approval(msg.sender, spender, value); + } +} + +contract Dummy { + fallback() external payable {} +} + +contract DummyGem is Dummy { + function transfer(address, uint) external pure returns(bool) { + return true; + } + + function transferFrom(address, address, uint) external pure returns(bool) { + return true; + } + + function decimals() external pure returns(uint) { + return 18; + } +} \ No newline at end of file diff --git a/packages/contracts/test/B.Protocol/BAMMTest.js b/packages/contracts/test/B.Protocol/BAMMTest.js index e7bdcf749..683fdeabe 100644 --- a/packages/contracts/test/B.Protocol/BAMMTest.js +++ b/packages/contracts/test/B.Protocol/BAMMTest.js @@ -54,7 +54,7 @@ contract('StabilityPool', async accounts => { const openTrove = async (params) => th.openTrove(contracts, params) const assertRevert = th.assertRevert - describe("Stability Pool Mechanisms", async () => { + describe("BAMM", async () => { before(async () => { gasPriceInWei = await web3.eth.getGasPrice() @@ -97,7 +97,7 @@ contract('StabilityPool', async accounts => { // --- provideToSP() --- // increases recorded LUSD at Stability Pool - it.only("deposit(): increases the Stability Pool LUSD balance", async () => { + it("deposit(): increases the Stability Pool LUSD balance", async () => { // --- SETUP --- Give Alice a least 200 await openTrove({ extraLUSDAmount: toBN(200), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) @@ -112,7 +112,7 @@ contract('StabilityPool', async accounts => { // --- provideToSP() --- // increases recorded LUSD at Stability Pool - it.only("deposit(): two users deposit, check their share", async () => { + it("deposit(): two users deposit, check their share", async () => { // --- SETUP --- Give Alice a least 200 await openTrove({ extraLUSDAmount: toBN(200), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) await openTrove({ extraLUSDAmount: toBN(200), ICR: toBN(dec(2, 18)), extraParams: { from: whale } }) @@ -132,7 +132,7 @@ contract('StabilityPool', async accounts => { // --- provideToSP() --- // increases recorded LUSD at Stability Pool - it.only("deposit(): two users deposit, one withdraw. check their share", async () => { + it("deposit(): two users deposit, one withdraw. check their share", async () => { // --- SETUP --- Give Alice a least 200 await openTrove({ extraLUSDAmount: toBN(200), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) await openTrove({ extraLUSDAmount: toBN(200), ICR: toBN(dec(2, 18)), extraParams: { from: whale } }) @@ -160,7 +160,7 @@ contract('StabilityPool', async accounts => { assert.equal(whaleBalanceAfter.sub(whaleBalanceBefore).toString(), 50) }) - it.only('rebalance scenario', async () => { + it('rebalance scenario', async () => { // --- SETUP --- // Whale opens Trove and deposits to SP @@ -214,7 +214,7 @@ contract('StabilityPool', async accounts => { assert.equal(swapBalance, ammExpectedEth.ethAmount) }) - it.only("test basic LQTY allocation", async () => { + it("test basic LQTY allocation", async () => { await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) // A, B, C, open troves @@ -255,7 +255,7 @@ contract('StabilityPool', async accounts => { assert.equal(D_LQTYBalance_After.add(E_LQTYBalance_After).toString(), F_LQTYBalance_After.toString()) }) - it.only("test complex LQTY allocation", async () => { + it("test complex LQTY allocation", async () => { await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) // A, B, C, open troves @@ -337,3533 +337,16 @@ contract('StabilityPool', async accounts => { assert.equal(D_LQTYBalance_After.toString(), A_LQTYBalance_After.toString()) assert.equal(E_LQTYBalance_After.toString(), A_LQTYBalance_After.mul(toBN(2)).toString()) assert.equal(F_LQTYBalance_After.toString(), B_LQTYBalance_After.toString()) - }) - - it("provideToSP(): reverts if user tries to provide more than their LUSD balance", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) - - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice, value: dec(50, 'ether') } }) - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob, value: dec(50, 'ether') } }) - const aliceLUSDbal = await lusdToken.balanceOf(alice) - const bobLUSDbal = await lusdToken.balanceOf(bob) - - // Alice, attempts to deposit 1 wei more than her balance - - const aliceTxPromise = stabilityPool.provideToSP(aliceLUSDbal.add(toBN(1)), frontEnd_1, { from: alice }) - await assertRevert(aliceTxPromise, "revert") - - // Bob, attempts to deposit 235534 more than his balance - - const bobTxPromise = stabilityPool.provideToSP(bobLUSDbal.add(toBN(dec(235534, 18))), frontEnd_1, { from: bob }) - await assertRevert(bobTxPromise, "revert") - }) - - it("provideToSP(): reverts if user tries to provide 2^256-1 LUSD, which exceeds their balance", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice, value: dec(50, 'ether') } }) - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob, value: dec(50, 'ether') } }) - - const maxBytes32 = web3.utils.toBN("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") - - // Alice attempts to deposit 2^256-1 LUSD - try { - aliceTx = await stabilityPool.provideToSP(maxBytes32, frontEnd_1, { from: alice }) - assert.isFalse(tx.receipt.status) - } catch (error) { - assert.include(error.message, "revert") - } - }) - - it("provideToSP(): reverts if cannot receive ETH Gain", async () => { - // --- SETUP --- - // Whale deposits 1850 LUSD in StabilityPool - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) - await stabilityPool.provideToSP(dec(1850, 18), frontEnd_1, { from: whale }) - - // Defaulter Troves opened - await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) - - // --- TEST --- - - const nonPayable = await NonPayable.new() - await lusdToken.transfer(nonPayable.address, dec(250, 18), { from: whale }) - - // NonPayable makes deposit #1: 150 LUSD - const txData1 = th.getTransactionData('provideToSP(uint256,address)', [web3.utils.toHex(dec(150, 18)), frontEnd_1]) - const tx1 = await nonPayable.forward(stabilityPool.address, txData1) - - const gain_0 = await stabilityPool.getDepositorETHGain(nonPayable.address) - assert.isTrue(gain_0.eq(toBN(0)), 'NonPayable should not have accumulated gains') - - // price drops: defaulters' Troves fall below MCR, nonPayable and whale Trove remain active - await priceFeed.setPrice(dec(105, 18)); - - // 2 defaulters are closed - await troveManager.liquidate(defaulter_1, { from: owner }) - await troveManager.liquidate(defaulter_2, { from: owner }) - - const gain_1 = await stabilityPool.getDepositorETHGain(nonPayable.address) - assert.isTrue(gain_1.gt(toBN(0)), 'NonPayable should have some accumulated gains') - - // NonPayable tries to make deposit #2: 100LUSD (which also attempts to withdraw ETH gain) - const txData2 = th.getTransactionData('provideToSP(uint256,address)', [web3.utils.toHex(dec(100, 18)), frontEnd_1]) - await th.assertRevert(nonPayable.forward(stabilityPool.address, txData2), 'StabilityPool: sending ETH failed') - }) - - it("provideToSP(): doesn't impact other users' deposits or ETH gains", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) - - // A, B, C open troves and make Stability Pool deposits - await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) - await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) - await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) - - await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: alice }) - await stabilityPool.provideToSP(dec(2000, 18), frontEnd_1, { from: bob }) - await stabilityPool.provideToSP(dec(3000, 18), frontEnd_1, { from: carol }) - - // D opens a trove - await openTrove({ extraLUSDAmount: toBN(dec(300, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: dennis } }) - - // Would-be defaulters open troves - await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) - - // Price drops - await priceFeed.setPrice(dec(105, 18)) - - // Defaulters are liquidated - await troveManager.liquidate(defaulter_1) - await troveManager.liquidate(defaulter_2) - assert.isFalse(await sortedTroves.contains(defaulter_1)) - assert.isFalse(await sortedTroves.contains(defaulter_2)) - - const alice_LUSDDeposit_Before = (await stabilityPool.getCompoundedLUSDDeposit(alice)).toString() - const bob_LUSDDeposit_Before = (await stabilityPool.getCompoundedLUSDDeposit(bob)).toString() - const carol_LUSDDeposit_Before = (await stabilityPool.getCompoundedLUSDDeposit(carol)).toString() - - const alice_ETHGain_Before = (await stabilityPool.getDepositorETHGain(alice)).toString() - const bob_ETHGain_Before = (await stabilityPool.getDepositorETHGain(bob)).toString() - const carol_ETHGain_Before = (await stabilityPool.getDepositorETHGain(carol)).toString() - - //check non-zero LUSD and ETHGain in the Stability Pool - const LUSDinSP = await stabilityPool.getTotalLUSDDeposits() - const ETHinSP = await stabilityPool.getETH() - assert.isTrue(LUSDinSP.gt(mv._zeroBN)) - assert.isTrue(ETHinSP.gt(mv._zeroBN)) - - // D makes an SP deposit - await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: dennis }) - assert.equal((await stabilityPool.getCompoundedLUSDDeposit(dennis)).toString(), dec(1000, 18)) - - const alice_LUSDDeposit_After = (await stabilityPool.getCompoundedLUSDDeposit(alice)).toString() - const bob_LUSDDeposit_After = (await stabilityPool.getCompoundedLUSDDeposit(bob)).toString() - const carol_LUSDDeposit_After = (await stabilityPool.getCompoundedLUSDDeposit(carol)).toString() - - const alice_ETHGain_After = (await stabilityPool.getDepositorETHGain(alice)).toString() - const bob_ETHGain_After = (await stabilityPool.getDepositorETHGain(bob)).toString() - const carol_ETHGain_After = (await stabilityPool.getDepositorETHGain(carol)).toString() - - // Check compounded deposits and ETH gains for A, B and C have not changed - assert.equal(alice_LUSDDeposit_Before, alice_LUSDDeposit_After) - assert.equal(bob_LUSDDeposit_Before, bob_LUSDDeposit_After) - assert.equal(carol_LUSDDeposit_Before, carol_LUSDDeposit_After) - - assert.equal(alice_ETHGain_Before, alice_ETHGain_After) - assert.equal(bob_ETHGain_Before, bob_ETHGain_After) - assert.equal(carol_ETHGain_Before, carol_ETHGain_After) - }) - - it("provideToSP(): doesn't impact system debt, collateral or TCR", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) - - // A, B, C open troves and make Stability Pool deposits - await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) - await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) - await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) - - await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: alice }) - await stabilityPool.provideToSP(dec(2000, 18), frontEnd_1, { from: bob }) - await stabilityPool.provideToSP(dec(3000, 18), frontEnd_1, { from: carol }) - - // D opens a trove - await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: dennis } }) - - // Would-be defaulters open troves - await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) - - // Price drops - await priceFeed.setPrice(dec(105, 18)) - - // Defaulters are liquidated - await troveManager.liquidate(defaulter_1) - await troveManager.liquidate(defaulter_2) - assert.isFalse(await sortedTroves.contains(defaulter_1)) - assert.isFalse(await sortedTroves.contains(defaulter_2)) - - const activeDebt_Before = (await activePool.getLUSDDebt()).toString() - const defaultedDebt_Before = (await defaultPool.getLUSDDebt()).toString() - const activeColl_Before = (await activePool.getETH()).toString() - const defaultedColl_Before = (await defaultPool.getETH()).toString() - const TCR_Before = (await th.getTCR(contracts)).toString() - - // D makes an SP deposit - await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: dennis }) - assert.equal((await stabilityPool.getCompoundedLUSDDeposit(dennis)).toString(), dec(1000, 18)) - - const activeDebt_After = (await activePool.getLUSDDebt()).toString() - const defaultedDebt_After = (await defaultPool.getLUSDDebt()).toString() - const activeColl_After = (await activePool.getETH()).toString() - const defaultedColl_After = (await defaultPool.getETH()).toString() - const TCR_After = (await th.getTCR(contracts)).toString() - - // Check total system debt, collateral and TCR have not changed after a Stability deposit is made - assert.equal(activeDebt_Before, activeDebt_After) - assert.equal(defaultedDebt_Before, defaultedDebt_After) - assert.equal(activeColl_Before, activeColl_After) - assert.equal(defaultedColl_Before, defaultedColl_After) - assert.equal(TCR_Before, TCR_After) - }) - - it("provideToSP(): doesn't impact any troves, including the caller's trove", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) - - // A, B, C open troves and make Stability Pool deposits - await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) - await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) - await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) - - // A and B provide to SP - await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: alice }) - await stabilityPool.provideToSP(dec(2000, 18), frontEnd_1, { from: bob }) - - // D opens a trove - await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: dennis } }) - - // Price drops - await priceFeed.setPrice(dec(105, 18)) - const price = await priceFeed.getPrice() - - // Get debt, collateral and ICR of all existing troves - const whale_Debt_Before = (await troveManager.Troves(whale))[0].toString() - const alice_Debt_Before = (await troveManager.Troves(alice))[0].toString() - const bob_Debt_Before = (await troveManager.Troves(bob))[0].toString() - const carol_Debt_Before = (await troveManager.Troves(carol))[0].toString() - const dennis_Debt_Before = (await troveManager.Troves(dennis))[0].toString() - - const whale_Coll_Before = (await troveManager.Troves(whale))[1].toString() - const alice_Coll_Before = (await troveManager.Troves(alice))[1].toString() - const bob_Coll_Before = (await troveManager.Troves(bob))[1].toString() - const carol_Coll_Before = (await troveManager.Troves(carol))[1].toString() - const dennis_Coll_Before = (await troveManager.Troves(dennis))[1].toString() - - const whale_ICR_Before = (await troveManager.getCurrentICR(whale, price)).toString() - const alice_ICR_Before = (await troveManager.getCurrentICR(alice, price)).toString() - const bob_ICR_Before = (await troveManager.getCurrentICR(bob, price)).toString() - const carol_ICR_Before = (await troveManager.getCurrentICR(carol, price)).toString() - const dennis_ICR_Before = (await troveManager.getCurrentICR(dennis, price)).toString() - - // D makes an SP deposit - await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: dennis }) - assert.equal((await stabilityPool.getCompoundedLUSDDeposit(dennis)).toString(), dec(1000, 18)) - - const whale_Debt_After = (await troveManager.Troves(whale))[0].toString() - const alice_Debt_After = (await troveManager.Troves(alice))[0].toString() - const bob_Debt_After = (await troveManager.Troves(bob))[0].toString() - const carol_Debt_After = (await troveManager.Troves(carol))[0].toString() - const dennis_Debt_After = (await troveManager.Troves(dennis))[0].toString() - - const whale_Coll_After = (await troveManager.Troves(whale))[1].toString() - const alice_Coll_After = (await troveManager.Troves(alice))[1].toString() - const bob_Coll_After = (await troveManager.Troves(bob))[1].toString() - const carol_Coll_After = (await troveManager.Troves(carol))[1].toString() - const dennis_Coll_After = (await troveManager.Troves(dennis))[1].toString() - - const whale_ICR_After = (await troveManager.getCurrentICR(whale, price)).toString() - const alice_ICR_After = (await troveManager.getCurrentICR(alice, price)).toString() - const bob_ICR_After = (await troveManager.getCurrentICR(bob, price)).toString() - const carol_ICR_After = (await troveManager.getCurrentICR(carol, price)).toString() - const dennis_ICR_After = (await troveManager.getCurrentICR(dennis, price)).toString() - - assert.equal(whale_Debt_Before, whale_Debt_After) - assert.equal(alice_Debt_Before, alice_Debt_After) - assert.equal(bob_Debt_Before, bob_Debt_After) - assert.equal(carol_Debt_Before, carol_Debt_After) - assert.equal(dennis_Debt_Before, dennis_Debt_After) - - assert.equal(whale_Coll_Before, whale_Coll_After) - assert.equal(alice_Coll_Before, alice_Coll_After) - assert.equal(bob_Coll_Before, bob_Coll_After) - assert.equal(carol_Coll_Before, carol_Coll_After) - assert.equal(dennis_Coll_Before, dennis_Coll_After) - - assert.equal(whale_ICR_Before, whale_ICR_After) - assert.equal(alice_ICR_Before, alice_ICR_After) - assert.equal(bob_ICR_Before, bob_ICR_After) - assert.equal(carol_ICR_Before, carol_ICR_After) - assert.equal(dennis_ICR_Before, dennis_ICR_After) - }) - - it("provideToSP(): doesn't protect the depositor's trove from liquidation", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) - - // A, B, C open troves and make Stability Pool deposits - await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) - await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) - await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) - - // A, B provide 100 LUSD to SP - await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: alice }) - await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: bob }) - - // Confirm Bob has an active trove in the system - assert.isTrue(await sortedTroves.contains(bob)) - assert.equal((await troveManager.getTroveStatus(bob)).toString(), '1') // Confirm Bob's trove status is active - - // Confirm Bob has a Stability deposit - assert.equal((await stabilityPool.getCompoundedLUSDDeposit(bob)).toString(), dec(1000, 18)) - - // Price drops - await priceFeed.setPrice(dec(105, 18)) - const price = await priceFeed.getPrice() - - // Liquidate bob - await troveManager.liquidate(bob) - - // Check Bob's trove has been removed from the system - assert.isFalse(await sortedTroves.contains(bob)) - assert.equal((await troveManager.getTroveStatus(bob)).toString(), '3') // check Bob's trove status was closed by liquidation - }) - - it("provideToSP(): providing 0 LUSD reverts", async () => { - // --- SETUP --- - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) - - // A, B, C open troves and make Stability Pool deposits - await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) - await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) - await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) - - // A, B, C provides 100, 50, 30 LUSD to SP - await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: alice }) - await stabilityPool.provideToSP(dec(50, 18), frontEnd_1, { from: bob }) - await stabilityPool.provideToSP(dec(30, 18), frontEnd_1, { from: carol }) - - const bob_Deposit_Before = (await stabilityPool.getCompoundedLUSDDeposit(bob)).toString() - const LUSDinSP_Before = (await stabilityPool.getTotalLUSDDeposits()).toString() - - assert.equal(LUSDinSP_Before, dec(180, 18)) - - // Bob provides 0 LUSD to the Stability Pool - const txPromise_B = stabilityPool.provideToSP(0, frontEnd_1, { from: bob }) - await th.assertRevert(txPromise_B) - }) - - // --- LQTY functionality --- - it("provideToSP(), new deposit: when SP > 0, triggers LQTY reward event - increases the sum G", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) - - // A, B, C open troves and make Stability Pool deposits - await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - - // A provides to SP - await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: A }) - - let currentEpoch = await stabilityPool.currentEpoch() - let currentScale = await stabilityPool.currentScale() - const G_Before = await stabilityPool.epochToScaleToG(currentEpoch, currentScale) - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // B provides to SP - await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: B }) - - currentEpoch = await stabilityPool.currentEpoch() - currentScale = await stabilityPool.currentScale() - const G_After = await stabilityPool.epochToScaleToG(currentEpoch, currentScale) - - // Expect G has increased from the LQTY reward event triggered - assert.isTrue(G_After.gt(G_Before)) - }) - - it("provideToSP(), new deposit: when SP is empty, doesn't update G", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) - - // A, B, C open troves and make Stability Pool deposits - await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - - // A provides to SP - await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: A }) - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // A withdraws - await stabilityPool.withdrawFromSP(dec(1000, 18), { from: A }) - - // Check SP is empty - assert.equal((await stabilityPool.getTotalLUSDDeposits()), '0') - - // Check G is non-zero - let currentEpoch = await stabilityPool.currentEpoch() - let currentScale = await stabilityPool.currentScale() - const G_Before = await stabilityPool.epochToScaleToG(currentEpoch, currentScale) - - assert.isTrue(G_Before.gt(toBN('0'))) - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // B provides to SP - await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: B }) - - currentEpoch = await stabilityPool.currentEpoch() - currentScale = await stabilityPool.currentScale() - const G_After = await stabilityPool.epochToScaleToG(currentEpoch, currentScale) - - // Expect G has not changed - assert.isTrue(G_After.eq(G_Before)) - }) - - it("provideToSP(), new deposit: sets the correct front end tag", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) - - // A, B, C, D open troves and make Stability Pool deposits - await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) - - // Check A, B, C D have no front end tags - const A_tagBefore = await getFrontEndTag(stabilityPool, A) - const B_tagBefore = await getFrontEndTag(stabilityPool, B) - const C_tagBefore = await getFrontEndTag(stabilityPool, C) - const D_tagBefore = await getFrontEndTag(stabilityPool, D) - - assert.equal(A_tagBefore, ZERO_ADDRESS) - assert.equal(B_tagBefore, ZERO_ADDRESS) - assert.equal(C_tagBefore, ZERO_ADDRESS) - assert.equal(D_tagBefore, ZERO_ADDRESS) - - // A, B, C, D provides to SP - await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(2000, 18), frontEnd_2, { from: B }) - await stabilityPool.provideToSP(dec(3000, 18), frontEnd_3, { from: C }) - await stabilityPool.provideToSP(dec(4000, 18), ZERO_ADDRESS, { from: D }) // transacts directly, no front end - - // Check A, B, C D have no front end tags - const A_tagAfter = await getFrontEndTag(stabilityPool, A) - const B_tagAfter = await getFrontEndTag(stabilityPool, B) - const C_tagAfter = await getFrontEndTag(stabilityPool, C) - const D_tagAfter = await getFrontEndTag(stabilityPool, D) - - // Check front end tags are correctly set - assert.equal(A_tagAfter, frontEnd_1) - assert.equal(B_tagAfter, frontEnd_2) - assert.equal(C_tagAfter, frontEnd_3) - assert.equal(D_tagAfter, ZERO_ADDRESS) - }) - - it("provideToSP(), new deposit: depositor does not receive any LQTY rewards", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) - - // A, B, open troves - await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - - // Get A, B, C LQTY balances before and confirm they're zero - const A_LQTYBalance_Before = await lqtyToken.balanceOf(A) - const B_LQTYBalance_Before = await lqtyToken.balanceOf(B) - - assert.equal(A_LQTYBalance_Before, '0') - assert.equal(B_LQTYBalance_Before, '0') - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // A, B provide to SP - await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(2000, 18), ZERO_ADDRESS, { from: B }) - - // Get A, B, C LQTY balances after, and confirm they're still zero - const A_LQTYBalance_After = await lqtyToken.balanceOf(A) - const B_LQTYBalance_After = await lqtyToken.balanceOf(B) - - assert.equal(A_LQTYBalance_After, '0') - assert.equal(B_LQTYBalance_After, '0') - }) - - it("provideToSP(), new deposit after past full withdrawal: depositor does not receive any LQTY rewards", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C, open troves - await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - await openTrove({ extraLUSDAmount: toBN(dec(4000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) - - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // --- SETUP --- - - const initialDeposit_A = await lusdToken.balanceOf(A) - const initialDeposit_B = await lusdToken.balanceOf(B) - // A, B provide to SP - await stabilityPool.provideToSP(initialDeposit_A, frontEnd_1, { from: A }) - await stabilityPool.provideToSP(initialDeposit_B, frontEnd_2, { from: B }) - - // time passes - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // C deposits. A, and B earn LQTY - await stabilityPool.provideToSP(dec(5, 18), ZERO_ADDRESS, { from: C }) - - // Price drops, defaulter is liquidated, A, B and C earn ETH - await priceFeed.setPrice(dec(105, 18)) - assert.isFalse(await th.checkRecoveryMode(contracts)) - - await troveManager.liquidate(defaulter_1) - - // price bounces back to 200 - await priceFeed.setPrice(dec(200, 18)) - - // A and B fully withdraw from the pool - await stabilityPool.withdrawFromSP(initialDeposit_A, { from: A }) - await stabilityPool.withdrawFromSP(initialDeposit_B, { from: B }) - - // --- TEST --- - - // Get A, B, C LQTY balances before and confirm they're non-zero - const A_LQTYBalance_Before = await lqtyToken.balanceOf(A) - const B_LQTYBalance_Before = await lqtyToken.balanceOf(B) - assert.isTrue(A_LQTYBalance_Before.gt(toBN('0'))) - assert.isTrue(B_LQTYBalance_Before.gt(toBN('0'))) - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // A, B provide to SP - await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(200, 18), ZERO_ADDRESS, { from: B }) - - // Get A, B, C LQTY balances after, and confirm they have not changed - const A_LQTYBalance_After = await lqtyToken.balanceOf(A) - const B_LQTYBalance_After = await lqtyToken.balanceOf(B) - - assert.isTrue(A_LQTYBalance_After.eq(A_LQTYBalance_Before)) - assert.isTrue(B_LQTYBalance_After.eq(B_LQTYBalance_Before)) - }) - - it("provideToSP(), new eligible deposit: tagged front end receives LQTY rewards", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C, open troves - await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) - await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: E } }) - await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: F } }) - - // D, E, F provide to SP - await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: D }) - await stabilityPool.provideToSP(dec(2000, 18), frontEnd_2, { from: E }) - await stabilityPool.provideToSP(dec(3000, 18), frontEnd_3, { from: F }) - - // Get F1, F2, F3 LQTY balances before, and confirm they're zero - const frontEnd_1_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_1) - const frontEnd_2_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_2) - const frontEnd_3_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_3) - - assert.equal(frontEnd_1_LQTYBalance_Before, '0') - assert.equal(frontEnd_2_LQTYBalance_Before, '0') - assert.equal(frontEnd_3_LQTYBalance_Before, '0') - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // console.log(`LQTYSupplyCap before: ${await communityIssuance.LQTYSupplyCap()}`) - // console.log(`totalLQTYIssued before: ${await communityIssuance.totalLQTYIssued()}`) - // console.log(`LQTY balance of CI before: ${await lqtyToken.balanceOf(communityIssuance.address)}`) - - // A, B, C provide to SP - await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(2000, 18), frontEnd_2, { from: B }) - await stabilityPool.provideToSP(dec(3000, 18), frontEnd_3, { from: C }) - - // console.log(`LQTYSupplyCap after: ${await communityIssuance.LQTYSupplyCap()}`) - // console.log(`totalLQTYIssued after: ${await communityIssuance.totalLQTYIssued()}`) - // console.log(`LQTY balance of CI after: ${await lqtyToken.balanceOf(communityIssuance.address)}`) - - // Get F1, F2, F3 LQTY balances after, and confirm they have increased - const frontEnd_1_LQTYBalance_After = await lqtyToken.balanceOf(frontEnd_1) - const frontEnd_2_LQTYBalance_After = await lqtyToken.balanceOf(frontEnd_2) - const frontEnd_3_LQTYBalance_After = await lqtyToken.balanceOf(frontEnd_3) - - assert.isTrue(frontEnd_1_LQTYBalance_After.gt(frontEnd_1_LQTYBalance_Before)) - assert.isTrue(frontEnd_2_LQTYBalance_After.gt(frontEnd_2_LQTYBalance_Before)) - assert.isTrue(frontEnd_3_LQTYBalance_After.gt(frontEnd_3_LQTYBalance_Before)) - }) - - it("provideToSP(), new eligible deposit: tagged front end's stake increases", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C, open troves - await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - - // Get front ends' stakes before - const F1_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_1) - const F2_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_2) - const F3_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_3) - - const deposit_A = dec(1000, 18) - const deposit_B = dec(2000, 18) - const deposit_C = dec(3000, 18) - - // A, B, C provide to SP - await stabilityPool.provideToSP(deposit_A, frontEnd_1, { from: A }) - await stabilityPool.provideToSP(deposit_B, frontEnd_2, { from: B }) - await stabilityPool.provideToSP(deposit_C, frontEnd_3, { from: C }) - - // Get front ends' stakes after - const F1_Stake_After = await stabilityPool.frontEndStakes(frontEnd_1) - const F2_Stake_After = await stabilityPool.frontEndStakes(frontEnd_2) - const F3_Stake_After = await stabilityPool.frontEndStakes(frontEnd_3) - - const F1_Diff = F1_Stake_After.sub(F1_Stake_Before) - const F2_Diff = F2_Stake_After.sub(F2_Stake_Before) - const F3_Diff = F3_Stake_After.sub(F3_Stake_Before) - - // Check front ends' stakes have increased by amount equal to the deposit made through them - assert.equal(F1_Diff, deposit_A) - assert.equal(F2_Diff, deposit_B) - assert.equal(F3_Diff, deposit_C) - }) - - it("provideToSP(), new eligible deposit: tagged front end's snapshots update", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C, open troves - await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - - // D opens trove - await openTrove({ extraLUSDAmount: toBN(dec(4000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) - - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // --- SETUP --- - - await stabilityPool.provideToSP(dec(2000, 18), ZERO_ADDRESS, { from: D }) - - // fastforward time then make an SP deposit, to make G > 0 - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - await stabilityPool.provideToSP(dec(2000, 18), ZERO_ADDRESS, { from: D }) - - // Perform a liquidation to make 0 < P < 1, and S > 0 - await priceFeed.setPrice(dec(105, 18)) - assert.isFalse(await th.checkRecoveryMode(contracts)) - - await troveManager.liquidate(defaulter_1) - - const currentEpoch = await stabilityPool.currentEpoch() - const currentScale = await stabilityPool.currentScale() - - const S_Before = await stabilityPool.epochToScaleToSum(currentEpoch, currentScale) - const P_Before = await stabilityPool.P() - const G_Before = await stabilityPool.epochToScaleToG(currentEpoch, currentScale) - - // Confirm 0 < P < 1 - assert.isTrue(P_Before.gt(toBN('0')) && P_Before.lt(toBN(dec(1, 18)))) - // Confirm S, G are both > 0 - assert.isTrue(S_Before.gt(toBN('0'))) - assert.isTrue(G_Before.gt(toBN('0'))) - - // Get front ends' snapshots before - for (frontEnd of [frontEnd_1, frontEnd_2, frontEnd_3]) { - const snapshot = await stabilityPool.frontEndSnapshots(frontEnd) - - assert.equal(snapshot[0], '0') // S (should always be 0 for front ends, since S corresponds to ETH gain) - assert.equal(snapshot[1], '0') // P - assert.equal(snapshot[2], '0') // G - assert.equal(snapshot[3], '0') // scale - assert.equal(snapshot[4], '0') // epoch - } - - const deposit_A = dec(1000, 18) - const deposit_B = dec(2000, 18) - const deposit_C = dec(3000, 18) - - // --- TEST --- - - // A, B, C provide to SP - const G1 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) - await stabilityPool.provideToSP(deposit_A, frontEnd_1, { from: A }) - - const G2 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) - await stabilityPool.provideToSP(deposit_B, frontEnd_2, { from: B }) - - const G3 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) - await stabilityPool.provideToSP(deposit_C, frontEnd_3, { from: C }) - - const frontEnds = [frontEnd_1, frontEnd_2, frontEnd_3] - const G_Values = [G1, G2, G3] - - // Map frontEnds to the value of G at time the deposit was made - frontEndToG = th.zipToObject(frontEnds, G_Values) - - // Get front ends' snapshots after - for (const [frontEnd, G] of Object.entries(frontEndToG)) { - const snapshot = await stabilityPool.frontEndSnapshots(frontEnd) - - // Check snapshots are the expected values - assert.equal(snapshot[0], '0') // S (should always be 0 for front ends) - assert.isTrue(snapshot[1].eq(P_Before)) // P - assert.isTrue(snapshot[2].eq(G)) // G - assert.equal(snapshot[3], '0') // scale - assert.equal(snapshot[4], '0') // epoch - } - }) - - it("provideToSP(), new deposit: depositor does not receive ETH gains", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // Whale transfers LUSD to A, B - await lusdToken.transfer(A, dec(100, 18), { from: whale }) - await lusdToken.transfer(B, dec(200, 18), { from: whale }) - - // C, D open troves - await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) - - // --- TEST --- - - // get current ETH balances - const A_ETHBalance_Before = await web3.eth.getBalance(A) - const B_ETHBalance_Before = await web3.eth.getBalance(B) - const C_ETHBalance_Before = await web3.eth.getBalance(C) - const D_ETHBalance_Before = await web3.eth.getBalance(D) - - // A, B, C, D provide to SP - await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: A, gasPrice: 0 }) - await stabilityPool.provideToSP(dec(200, 18), ZERO_ADDRESS, { from: B, gasPrice: 0 }) - await stabilityPool.provideToSP(dec(300, 18), frontEnd_2, { from: C, gasPrice: 0 }) - await stabilityPool.provideToSP(dec(400, 18), ZERO_ADDRESS, { from: D, gasPrice: 0 }) - - // Get ETH balances after - const A_ETHBalance_After = await web3.eth.getBalance(A) - const B_ETHBalance_After = await web3.eth.getBalance(B) - const C_ETHBalance_After = await web3.eth.getBalance(C) - const D_ETHBalance_After = await web3.eth.getBalance(D) - - // Check ETH balances have not changed - assert.equal(A_ETHBalance_After, A_ETHBalance_Before) - assert.equal(B_ETHBalance_After, B_ETHBalance_Before) - assert.equal(C_ETHBalance_After, C_ETHBalance_Before) - assert.equal(D_ETHBalance_After, D_ETHBalance_Before) - }) - - it("provideToSP(), new deposit after past full withdrawal: depositor does not receive ETH gains", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // Whale transfers LUSD to A, B - await lusdToken.transfer(A, dec(1000, 18), { from: whale }) - await lusdToken.transfer(B, dec(1000, 18), { from: whale }) - - // C, D open troves - await openTrove({ extraLUSDAmount: toBN(dec(4000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - await openTrove({ extraLUSDAmount: toBN(dec(5000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) - - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // --- SETUP --- - // A, B, C, D provide to SP - await stabilityPool.provideToSP(dec(105, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(105, 18), ZERO_ADDRESS, { from: B }) - await stabilityPool.provideToSP(dec(105, 18), frontEnd_1, { from: C }) - await stabilityPool.provideToSP(dec(105, 18), ZERO_ADDRESS, { from: D }) - - // time passes - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // B deposits. A,B,C,D earn LQTY - await stabilityPool.provideToSP(dec(5, 18), ZERO_ADDRESS, { from: B }) - - // Price drops, defaulter is liquidated, A, B, C, D earn ETH - await priceFeed.setPrice(dec(105, 18)) - assert.isFalse(await th.checkRecoveryMode(contracts)) - - await troveManager.liquidate(defaulter_1) - - // Price bounces back - await priceFeed.setPrice(dec(200, 18)) - - // A B,C, D fully withdraw from the pool - await stabilityPool.withdrawFromSP(dec(105, 18), { from: A }) - await stabilityPool.withdrawFromSP(dec(105, 18), { from: B }) - await stabilityPool.withdrawFromSP(dec(105, 18), { from: C }) - await stabilityPool.withdrawFromSP(dec(105, 18), { from: D }) - - // --- TEST --- - - // get current ETH balances - const A_ETHBalance_Before = await web3.eth.getBalance(A) - const B_ETHBalance_Before = await web3.eth.getBalance(B) - const C_ETHBalance_Before = await web3.eth.getBalance(C) - const D_ETHBalance_Before = await web3.eth.getBalance(D) - - // A, B, C, D provide to SP - await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: A, gasPrice: 0 }) - await stabilityPool.provideToSP(dec(200, 18), ZERO_ADDRESS, { from: B, gasPrice: 0 }) - await stabilityPool.provideToSP(dec(300, 18), frontEnd_2, { from: C, gasPrice: 0 }) - await stabilityPool.provideToSP(dec(400, 18), ZERO_ADDRESS, { from: D, gasPrice: 0 }) - - // Get ETH balances after - const A_ETHBalance_After = await web3.eth.getBalance(A) - const B_ETHBalance_After = await web3.eth.getBalance(B) - const C_ETHBalance_After = await web3.eth.getBalance(C) - const D_ETHBalance_After = await web3.eth.getBalance(D) - - // Check ETH balances have not changed - assert.equal(A_ETHBalance_After, A_ETHBalance_Before) - assert.equal(B_ETHBalance_After, B_ETHBalance_Before) - assert.equal(C_ETHBalance_After, C_ETHBalance_Before) - assert.equal(D_ETHBalance_After, D_ETHBalance_Before) - }) - - it("provideToSP(), topup: triggers LQTY reward event - increases the sum G", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C open troves - await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - - // A, B, C provide to SP - await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(50, 18), frontEnd_1, { from: B }) - await stabilityPool.provideToSP(dec(50, 18), frontEnd_1, { from: C }) - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - const G_Before = await stabilityPool.epochToScaleToG(0, 0) - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // B tops up - await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: B }) - - const G_After = await stabilityPool.epochToScaleToG(0, 0) - - // Expect G has increased from the LQTY reward event triggered by B's topup - assert.isTrue(G_After.gt(G_Before)) - }) - - it("provideToSP(), topup from different front end: doesn't change the front end tag", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // whale transfer to troves D and E - await lusdToken.transfer(D, dec(100, 18), { from: whale }) - await lusdToken.transfer(E, dec(200, 18), { from: whale }) - - // A, B, C open troves - await openTrove({ extraLUSDAmount: toBN(dec(100, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(200, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(300, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - - - // A, B, C, D, E provide to SP - await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: B }) - await stabilityPool.provideToSP(dec(30, 18), ZERO_ADDRESS, { from: C }) - await stabilityPool.provideToSP(dec(40, 18), frontEnd_1, { from: D }) - await stabilityPool.provideToSP(dec(50, 18), ZERO_ADDRESS, { from: E }) - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // A, B, C, D, E top up, from different front ends - await stabilityPool.provideToSP(dec(10, 18), frontEnd_2, { from: A }) - await stabilityPool.provideToSP(dec(20, 18), frontEnd_1, { from: B }) - await stabilityPool.provideToSP(dec(15, 18), frontEnd_3, { from: C }) - await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: D }) - await stabilityPool.provideToSP(dec(30, 18), frontEnd_3, { from: E }) - - const frontEndTag_A = (await stabilityPool.deposits(A))[1] - const frontEndTag_B = (await stabilityPool.deposits(B))[1] - const frontEndTag_C = (await stabilityPool.deposits(C))[1] - const frontEndTag_D = (await stabilityPool.deposits(D))[1] - const frontEndTag_E = (await stabilityPool.deposits(E))[1] - - // Check deposits are still tagged with their original front end - assert.equal(frontEndTag_A, frontEnd_1) - assert.equal(frontEndTag_B, frontEnd_2) - assert.equal(frontEndTag_C, ZERO_ADDRESS) - assert.equal(frontEndTag_D, frontEnd_1) - assert.equal(frontEndTag_E, ZERO_ADDRESS) - }) - - it("provideToSP(), topup: depositor receives LQTY rewards", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C open troves - await openTrove({ extraLUSDAmount: toBN(dec(100, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(200, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(300, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - - // A, B, C, provide to SP - await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: B }) - await stabilityPool.provideToSP(dec(30, 18), ZERO_ADDRESS, { from: C }) - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // Get A, B, C LQTY balance before - const A_LQTYBalance_Before = await lqtyToken.balanceOf(A) - const B_LQTYBalance_Before = await lqtyToken.balanceOf(B) - const C_LQTYBalance_Before = await lqtyToken.balanceOf(C) - - // A, B, C top up - await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: B }) - await stabilityPool.provideToSP(dec(30, 18), ZERO_ADDRESS, { from: C }) - - // Get LQTY balance after - const A_LQTYBalance_After = await lqtyToken.balanceOf(A) - const B_LQTYBalance_After = await lqtyToken.balanceOf(B) - const C_LQTYBalance_After = await lqtyToken.balanceOf(C) - - // Check LQTY Balance of A, B, C has increased - assert.isTrue(A_LQTYBalance_After.gt(A_LQTYBalance_Before)) - assert.isTrue(B_LQTYBalance_After.gt(B_LQTYBalance_Before)) - assert.isTrue(C_LQTYBalance_After.gt(C_LQTYBalance_Before)) - }) - - it("provideToSP(), topup: tagged front end receives LQTY rewards", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C open troves - await openTrove({ extraLUSDAmount: toBN(dec(100, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(200, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(300, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - - // A, B, C, provide to SP - await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: B }) - await stabilityPool.provideToSP(dec(30, 18), frontEnd_3, { from: C }) - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // Get front ends' LQTY balance before - const F1_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_1) - const F2_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_2) - const F3_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_3) - - // A, B, C top up (front end param passed here is irrelevant) - await stabilityPool.provideToSP(dec(10, 18), ZERO_ADDRESS, { from: A }) // provides no front end param - await stabilityPool.provideToSP(dec(20, 18), frontEnd_1, { from: B }) // provides front end that doesn't match his tag - await stabilityPool.provideToSP(dec(30, 18), frontEnd_3, { from: C }) // provides front end that matches his tag - - // Get front ends' LQTY balance after - const F1_LQTYBalance_After = await lqtyToken.balanceOf(A) - const F2_LQTYBalance_After = await lqtyToken.balanceOf(B) - const F3_LQTYBalance_After = await lqtyToken.balanceOf(C) - - // Check LQTY Balance of front ends has increased - assert.isTrue(F1_LQTYBalance_After.gt(F1_LQTYBalance_Before)) - assert.isTrue(F2_LQTYBalance_After.gt(F2_LQTYBalance_Before)) - assert.isTrue(F3_LQTYBalance_After.gt(F3_LQTYBalance_Before)) - }) - - it("provideToSP(), topup: tagged front end's stake increases", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C, D, E, F open troves - await openTrove({ extraLUSDAmount: toBN(dec(100, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(200, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(300, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - await openTrove({ extraLUSDAmount: toBN(dec(100, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) - await openTrove({ extraLUSDAmount: toBN(dec(200, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: E } }) - await openTrove({ extraLUSDAmount: toBN(dec(300, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: F } }) - - // A, B, C, D, E, F provide to SP - await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: B }) - await stabilityPool.provideToSP(dec(30, 18), frontEnd_3, { from: C }) - await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: D }) - await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: E }) - await stabilityPool.provideToSP(dec(30, 18), frontEnd_3, { from: F }) - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // Get front ends' stake before - const F1_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_1) - const F2_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_2) - const F3_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_3) - - // A, B, C top up (front end param passed here is irrelevant) - await stabilityPool.provideToSP(dec(10, 18), ZERO_ADDRESS, { from: A }) // provides no front end param - await stabilityPool.provideToSP(dec(20, 18), frontEnd_1, { from: B }) // provides front end that doesn't match his tag - await stabilityPool.provideToSP(dec(30, 18), frontEnd_3, { from: C }) // provides front end that matches his tag - - // Get front ends' stakes after - const F1_Stake_After = await stabilityPool.frontEndStakes(frontEnd_1) - const F2_Stake_After = await stabilityPool.frontEndStakes(frontEnd_2) - const F3_Stake_After = await stabilityPool.frontEndStakes(frontEnd_3) - - // Check front ends' stakes have increased - assert.isTrue(F1_Stake_After.gt(F1_Stake_Before)) - assert.isTrue(F2_Stake_After.gt(F2_Stake_Before)) - assert.isTrue(F3_Stake_After.gt(F3_Stake_Before)) - }) - - it("provideToSP(), topup: tagged front end's snapshots update", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C, open troves - await openTrove({ extraLUSDAmount: toBN(dec(200, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(400, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(600, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - - // D opens trove - await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) - - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // --- SETUP --- - - const deposit_A = dec(100, 18) - const deposit_B = dec(200, 18) - const deposit_C = dec(300, 18) - - // A, B, C make their initial deposits - await stabilityPool.provideToSP(deposit_A, frontEnd_1, { from: A }) - await stabilityPool.provideToSP(deposit_B, frontEnd_2, { from: B }) - await stabilityPool.provideToSP(deposit_C, frontEnd_3, { from: C }) - - // fastforward time then make an SP deposit, to make G > 0 - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - await stabilityPool.provideToSP(await lusdToken.balanceOf(D), ZERO_ADDRESS, { from: D }) - - // perform a liquidation to make 0 < P < 1, and S > 0 - await priceFeed.setPrice(dec(100, 18)) - assert.isFalse(await th.checkRecoveryMode(contracts)) - - await troveManager.liquidate(defaulter_1) - - const currentEpoch = await stabilityPool.currentEpoch() - const currentScale = await stabilityPool.currentScale() - - const S_Before = await stabilityPool.epochToScaleToSum(currentEpoch, currentScale) - const P_Before = await stabilityPool.P() - const G_Before = await stabilityPool.epochToScaleToG(currentEpoch, currentScale) - - // Confirm 0 < P < 1 - assert.isTrue(P_Before.gt(toBN('0')) && P_Before.lt(toBN(dec(1, 18)))) - // Confirm S, G are both > 0 - assert.isTrue(S_Before.gt(toBN('0'))) - assert.isTrue(G_Before.gt(toBN('0'))) - - // Get front ends' snapshots before - for (frontEnd of [frontEnd_1, frontEnd_2, frontEnd_3]) { - const snapshot = await stabilityPool.frontEndSnapshots(frontEnd) - - assert.equal(snapshot[0], '0') // S (should always be 0 for front ends, since S corresponds to ETH gain) - assert.equal(snapshot[1], dec(1, 18)) // P - assert.equal(snapshot[2], '0') // G - assert.equal(snapshot[3], '0') // scale - assert.equal(snapshot[4], '0') // epoch - } - - // --- TEST --- - - // A, B, C top up their deposits. Grab G at each stage, as it can increase a bit - // between topups, because some block.timestamp time passes (and LQTY is issued) between ops - const G1 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) - await stabilityPool.provideToSP(deposit_A, frontEnd_1, { from: A }) - - const G2 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) - await stabilityPool.provideToSP(deposit_B, frontEnd_2, { from: B }) - - const G3 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) - await stabilityPool.provideToSP(deposit_C, frontEnd_3, { from: C }) - - const frontEnds = [frontEnd_1, frontEnd_2, frontEnd_3] - const G_Values = [G1, G2, G3] - - // Map frontEnds to the value of G at time the deposit was made - frontEndToG = th.zipToObject(frontEnds, G_Values) - - // Get front ends' snapshots after - for (const [frontEnd, G] of Object.entries(frontEndToG)) { - const snapshot = await stabilityPool.frontEndSnapshots(frontEnd) - - // Check snapshots are the expected values - assert.equal(snapshot[0], '0') // S (should always be 0 for front ends) - assert.isTrue(snapshot[1].eq(P_Before)) // P - assert.isTrue(snapshot[2].eq(G)) // G - assert.equal(snapshot[3], '0') // scale - assert.equal(snapshot[4], '0') // epoch - } - }) - - it("provideToSP(): reverts when amount is zero", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - - // Whale transfers LUSD to C, D - await lusdToken.transfer(C, dec(100, 18), { from: whale }) - await lusdToken.transfer(D, dec(100, 18), { from: whale }) - - txPromise_A = stabilityPool.provideToSP(0, frontEnd_1, { from: A }) - txPromise_B = stabilityPool.provideToSP(0, ZERO_ADDRESS, { from: B }) - txPromise_C = stabilityPool.provideToSP(0, frontEnd_2, { from: C }) - txPromise_D = stabilityPool.provideToSP(0, ZERO_ADDRESS, { from: D }) - - await th.assertRevert(txPromise_A, 'StabilityPool: Amount must be non-zero') - await th.assertRevert(txPromise_B, 'StabilityPool: Amount must be non-zero') - await th.assertRevert(txPromise_C, 'StabilityPool: Amount must be non-zero') - await th.assertRevert(txPromise_D, 'StabilityPool: Amount must be non-zero') - }) - - it("provideToSP(): reverts if user is a registered front end", async () => { - // C, D, E, F open troves - await openTrove({ extraLUSDAmount: toBN(dec(30, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - await openTrove({ extraLUSDAmount: toBN(dec(30, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) - await openTrove({ extraLUSDAmount: toBN(dec(30, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: E } }) - await openTrove({ extraLUSDAmount: toBN(dec(30, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: F } }) - - // C, E, F registers as front end - await stabilityPool.registerFrontEnd(dec(1, 18), { from: C }) - await stabilityPool.registerFrontEnd(dec(1, 18), { from: E }) - await stabilityPool.registerFrontEnd(dec(1, 18), { from: F }) - - const txPromise_C = stabilityPool.provideToSP(dec(10, 18), ZERO_ADDRESS, { from: C }) - const txPromise_E = stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: E }) - const txPromise_F = stabilityPool.provideToSP(dec(10, 18), F, { from: F }) - await th.assertRevert(txPromise_C, "StabilityPool: must not already be a registered front end") - await th.assertRevert(txPromise_E, "StabilityPool: must not already be a registered front end") - await th.assertRevert(txPromise_F, "StabilityPool: must not already be a registered front end") - - const txD = await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: D }) - assert.isTrue(txD.receipt.status) - }) - - it("provideToSP(): reverts if provided tag is not a registered front end", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(30, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - await openTrove({ extraLUSDAmount: toBN(dec(30, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) - await openTrove({ extraLUSDAmount: toBN(dec(30, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: E } }) - - const txPromise_C = stabilityPool.provideToSP(dec(10, 18), A, { from: C }) // passes another EOA - const txPromise_D = stabilityPool.provideToSP(dec(10, 18), troveManager.address, { from: D }) - const txPromise_E = stabilityPool.provideToSP(dec(10, 18), stabilityPool.address, { from: E }) - const txPromise_F = stabilityPool.provideToSP(dec(10, 18), F, { from: F }) // passes itself - - await th.assertRevert(txPromise_C, "StabilityPool: Tag must be a registered front end, or the zero address") - await th.assertRevert(txPromise_D, "StabilityPool: Tag must be a registered front end, or the zero address") - await th.assertRevert(txPromise_E, "StabilityPool: Tag must be a registered front end, or the zero address") - await th.assertRevert(txPromise_F, "StabilityPool: Tag must be a registered front end, or the zero address") - }) - - // --- withdrawFromSP --- - - it("withdrawFromSP(): reverts when user has no active deposit", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(100, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) - await openTrove({ extraLUSDAmount: toBN(dec(100, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) - - await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: alice }) - - const alice_initialDeposit = ((await stabilityPool.deposits(alice))[0]).toString() - const bob_initialDeposit = ((await stabilityPool.deposits(bob))[0]).toString() - - assert.equal(alice_initialDeposit, dec(100, 18)) - assert.equal(bob_initialDeposit, '0') - - const txAlice = await stabilityPool.withdrawFromSP(dec(100, 18), { from: alice }) - assert.isTrue(txAlice.receipt.status) - - - try { - const txBob = await stabilityPool.withdrawFromSP(dec(100, 18), { from: bob }) - assert.isFalse(txBob.receipt.status) - } catch (err) { - assert.include(err.message, "revert") - // TODO: infamous issue #99 - //assert.include(err.message, "User must have a non-zero deposit") - - } - }) - - it("withdrawFromSP(): reverts when amount > 0 and system has an undercollateralized trove", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(100, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) - - await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: alice }) - - const alice_initialDeposit = ((await stabilityPool.deposits(alice))[0]).toString() - assert.equal(alice_initialDeposit, dec(100, 18)) - - // defaulter opens trove - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // ETH drops, defaulter is in liquidation range (but not liquidated yet) - await priceFeed.setPrice(dec(100, 18)) - - await th.assertRevert(stabilityPool.withdrawFromSP(dec(100, 18), { from: alice })) - }) - - it("withdrawFromSP(): partial retrieval - retrieves correct LUSD amount and the entire ETH Gain, and updates deposit", async () => { - // --- SETUP --- - // Whale deposits 185000 LUSD in StabilityPool - await openTrove({ extraLUSDAmount: toBN(dec(1, 24)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - await stabilityPool.provideToSP(dec(185000, 18), frontEnd_1, { from: whale }) - - // 2 Troves opened - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) - - // --- TEST --- - - // Alice makes deposit #1: 15000 LUSD - await openTrove({ extraLUSDAmount: toBN(dec(15000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: alice } }) - await stabilityPool.provideToSP(dec(15000, 18), frontEnd_1, { from: alice }) - - // price drops: defaulters' Troves fall below MCR, alice and whale Trove remain active - await priceFeed.setPrice(dec(105, 18)); - - // 2 users with Trove with 170 LUSD drawn are closed - const liquidationTX_1 = await troveManager.liquidate(defaulter_1, { from: owner }) // 170 LUSD closed - const liquidationTX_2 = await troveManager.liquidate(defaulter_2, { from: owner }) // 170 LUSD closed - - const [liquidatedDebt_1] = await th.getEmittedLiquidationValues(liquidationTX_1) - const [liquidatedDebt_2] = await th.getEmittedLiquidationValues(liquidationTX_2) - - // Alice LUSDLoss is ((15000/200000) * liquidatedDebt), for each liquidation - const expectedLUSDLoss_A = (liquidatedDebt_1.mul(toBN(dec(15000, 18))).div(toBN(dec(200000, 18)))) - .add(liquidatedDebt_2.mul(toBN(dec(15000, 18))).div(toBN(dec(200000, 18)))) - - const expectedCompoundedLUSDDeposit_A = toBN(dec(15000, 18)).sub(expectedLUSDLoss_A) - const compoundedLUSDDeposit_A = await stabilityPool.getCompoundedLUSDDeposit(alice) - - assert.isAtMost(th.getDifference(expectedCompoundedLUSDDeposit_A, compoundedLUSDDeposit_A), 100000) - - // Alice retrieves part of her entitled LUSD: 9000 LUSD - await stabilityPool.withdrawFromSP(dec(9000, 18), { from: alice }) - - const expectedNewDeposit_A = (compoundedLUSDDeposit_A.sub(toBN(dec(9000, 18)))) - - // check Alice's deposit has been updated to equal her compounded deposit minus her withdrawal */ - const newDeposit = ((await stabilityPool.deposits(alice))[0]).toString() - assert.isAtMost(th.getDifference(newDeposit, expectedNewDeposit_A), 100000) - - // Expect Alice has withdrawn all ETH gain - const alice_pendingETHGain = await stabilityPool.getDepositorETHGain(alice) - assert.equal(alice_pendingETHGain, 0) - }) - - it("withdrawFromSP(): partial retrieval - leaves the correct amount of LUSD in the Stability Pool", async () => { - // --- SETUP --- - // Whale deposits 185000 LUSD in StabilityPool - await openTrove({ extraLUSDAmount: toBN(dec(1, 24)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - await stabilityPool.provideToSP(dec(185000, 18), frontEnd_1, { from: whale }) - - // 2 Troves opened - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) - // --- TEST --- - - // Alice makes deposit #1: 15000 LUSD - await openTrove({ extraLUSDAmount: toBN(dec(15000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: alice } }) - await stabilityPool.provideToSP(dec(15000, 18), frontEnd_1, { from: alice }) - - const SP_LUSD_Before = await stabilityPool.getTotalLUSDDeposits() - assert.equal(SP_LUSD_Before, dec(200000, 18)) - - // price drops: defaulters' Troves fall below MCR, alice and whale Trove remain active - await priceFeed.setPrice(dec(105, 18)); - - // 2 users liquidated - const liquidationTX_1 = await troveManager.liquidate(defaulter_1, { from: owner }) - const liquidationTX_2 = await troveManager.liquidate(defaulter_2, { from: owner }) - - const [liquidatedDebt_1] = await th.getEmittedLiquidationValues(liquidationTX_1) - const [liquidatedDebt_2] = await th.getEmittedLiquidationValues(liquidationTX_2) - - // Alice retrieves part of her entitled LUSD: 9000 LUSD - await stabilityPool.withdrawFromSP(dec(9000, 18), { from: alice }) - - /* Check SP has reduced from 2 liquidations and Alice's withdrawal - Expect LUSD in SP = (200000 - liquidatedDebt_1 - liquidatedDebt_2 - 9000) */ - const expectedSPLUSD = toBN(dec(200000, 18)) - .sub(toBN(liquidatedDebt_1)) - .sub(toBN(liquidatedDebt_2)) - .sub(toBN(dec(9000, 18))) - - const SP_LUSD_After = (await stabilityPool.getTotalLUSDDeposits()).toString() - - th.assertIsApproximatelyEqual(SP_LUSD_After, expectedSPLUSD) - }) - - it("withdrawFromSP(): full retrieval - leaves the correct amount of LUSD in the Stability Pool", async () => { - // --- SETUP --- - // Whale deposits 185000 LUSD in StabilityPool - await openTrove({ extraLUSDAmount: toBN(dec(1000000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - await stabilityPool.provideToSP(dec(185000, 18), frontEnd_1, { from: whale }) - - // 2 Troves opened - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) - - // --- TEST --- - - // Alice makes deposit #1 - await openTrove({ extraLUSDAmount: toBN(dec(15000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: alice } }) - await stabilityPool.provideToSP(dec(15000, 18), frontEnd_1, { from: alice }) - - const SP_LUSD_Before = await stabilityPool.getTotalLUSDDeposits() - assert.equal(SP_LUSD_Before, dec(200000, 18)) - - // price drops: defaulters' Troves fall below MCR, alice and whale Trove remain active - await priceFeed.setPrice(dec(105, 18)); - - // 2 defaulters liquidated - const liquidationTX_1 = await troveManager.liquidate(defaulter_1, { from: owner }) - const liquidationTX_2 = await troveManager.liquidate(defaulter_2, { from: owner }) - - const [liquidatedDebt_1] = await th.getEmittedLiquidationValues(liquidationTX_1) - const [liquidatedDebt_2] = await th.getEmittedLiquidationValues(liquidationTX_2) - - // Alice LUSDLoss is ((15000/200000) * liquidatedDebt), for each liquidation - const expectedLUSDLoss_A = (liquidatedDebt_1.mul(toBN(dec(15000, 18))).div(toBN(dec(200000, 18)))) - .add(liquidatedDebt_2.mul(toBN(dec(15000, 18))).div(toBN(dec(200000, 18)))) - - const expectedCompoundedLUSDDeposit_A = toBN(dec(15000, 18)).sub(expectedLUSDLoss_A) - const compoundedLUSDDeposit_A = await stabilityPool.getCompoundedLUSDDeposit(alice) - - assert.isAtMost(th.getDifference(expectedCompoundedLUSDDeposit_A, compoundedLUSDDeposit_A), 100000) - - const LUSDinSPBefore = await stabilityPool.getTotalLUSDDeposits() - - // Alice retrieves all of her entitled LUSD: - await stabilityPool.withdrawFromSP(dec(15000, 18), { from: alice }) - - const expectedLUSDinSPAfter = LUSDinSPBefore.sub(compoundedLUSDDeposit_A) - - const LUSDinSPAfter = await stabilityPool.getTotalLUSDDeposits() - assert.isAtMost(th.getDifference(expectedLUSDinSPAfter, LUSDinSPAfter), 100000) - }) - - it("withdrawFromSP(): Subsequent deposit and withdrawal attempt from same account, with no intermediate liquidations, withdraws zero ETH", async () => { - // --- SETUP --- - // Whale deposits 1850 LUSD in StabilityPool - await openTrove({ extraLUSDAmount: toBN(dec(1000000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - await stabilityPool.provideToSP(dec(18500, 18), frontEnd_1, { from: whale }) - - // 2 defaulters open - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) - - // --- TEST --- - - // Alice makes deposit #1: 15000 LUSD - await openTrove({ extraLUSDAmount: toBN(dec(15000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: alice } }) - await stabilityPool.provideToSP(dec(15000, 18), frontEnd_1, { from: alice }) - - // price drops: defaulters' Troves fall below MCR, alice and whale Trove remain active - await priceFeed.setPrice(dec(105, 18)); - - // defaulters liquidated - await troveManager.liquidate(defaulter_1, { from: owner }) - await troveManager.liquidate(defaulter_2, { from: owner }) - - // Alice retrieves all of her entitled LUSD: - await stabilityPool.withdrawFromSP(dec(15000, 18), { from: alice }) - assert.equal(await stabilityPool.getDepositorETHGain(alice), 0) - - // Alice makes second deposit - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: alice }) - assert.equal(await stabilityPool.getDepositorETHGain(alice), 0) - - const ETHinSP_Before = (await stabilityPool.getETH()).toString() - - // Alice attempts second withdrawal - await stabilityPool.withdrawFromSP(dec(10000, 18), { from: alice }) - assert.equal(await stabilityPool.getDepositorETHGain(alice), 0) - - // Check ETH in pool does not change - const ETHinSP_1 = (await stabilityPool.getETH()).toString() - assert.equal(ETHinSP_Before, ETHinSP_1) - - // Third deposit - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: alice }) - assert.equal(await stabilityPool.getDepositorETHGain(alice), 0) - - // Alice attempts third withdrawal (this time, frm SP to Trove) - const txPromise_A = stabilityPool.withdrawETHGainToTrove(alice, alice, { from: alice }) - await th.assertRevert(txPromise_A) - }) - - it("withdrawFromSP(): it correctly updates the user's LUSD and ETH snapshots of entitled reward per unit staked", async () => { - // --- SETUP --- - // Whale deposits 185000 LUSD in StabilityPool - await openTrove({ extraLUSDAmount: toBN(dec(1000000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - await stabilityPool.provideToSP(dec(185000, 18), frontEnd_1, { from: whale }) - - // 2 defaulters open - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) - - // --- TEST --- - - // Alice makes deposit #1: 15000 LUSD - await openTrove({ extraLUSDAmount: toBN(dec(15000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: alice } }) - await stabilityPool.provideToSP(dec(15000, 18), frontEnd_1, { from: alice }) - - // check 'Before' snapshots - const alice_snapshot_Before = await stabilityPool.depositSnapshots(alice) - const alice_snapshot_S_Before = alice_snapshot_Before[0].toString() - const alice_snapshot_P_Before = alice_snapshot_Before[1].toString() - assert.equal(alice_snapshot_S_Before, 0) - assert.equal(alice_snapshot_P_Before, '1000000000000000000') - - // price drops: defaulters' Troves fall below MCR, alice and whale Trove remain active - await priceFeed.setPrice(dec(105, 18)); - - // 2 defaulters liquidated - await troveManager.liquidate(defaulter_1, { from: owner }) - await troveManager.liquidate(defaulter_2, { from: owner }); - - // Alice retrieves part of her entitled LUSD: 9000 LUSD - await stabilityPool.withdrawFromSP(dec(9000, 18), { from: alice }) - - const P = (await stabilityPool.P()).toString() - const S = (await stabilityPool.epochToScaleToSum(0, 0)).toString() - // check 'After' snapshots - const alice_snapshot_After = await stabilityPool.depositSnapshots(alice) - const alice_snapshot_S_After = alice_snapshot_After[0].toString() - const alice_snapshot_P_After = alice_snapshot_After[1].toString() - assert.equal(alice_snapshot_S_After, S) - assert.equal(alice_snapshot_P_After, P) - }) - - it("withdrawFromSP(): decreases StabilityPool ETH", async () => { - // --- SETUP --- - // Whale deposits 185000 LUSD in StabilityPool - await openTrove({ extraLUSDAmount: toBN(dec(1000000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - await stabilityPool.provideToSP(dec(185000, 18), frontEnd_1, { from: whale }) - - // 1 defaulter opens - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // --- TEST --- - - // Alice makes deposit #1: 15000 LUSD - await openTrove({ extraLUSDAmount: toBN(dec(15000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: alice } }) - await stabilityPool.provideToSP(dec(15000, 18), frontEnd_1, { from: alice }) - - // price drops: defaulter's Trove falls below MCR, alice and whale Trove remain active - await priceFeed.setPrice('100000000000000000000'); - - // defaulter's Trove is closed. - const liquidationTx_1 = await troveManager.liquidate(defaulter_1, { from: owner }) // 180 LUSD closed - const [, liquidatedColl,] = th.getEmittedLiquidationValues(liquidationTx_1) - - //Get ActivePool and StabilityPool Ether before retrieval: - const active_ETH_Before = await activePool.getETH() - const stability_ETH_Before = await stabilityPool.getETH() - - // Expect alice to be entitled to 15000/200000 of the liquidated coll - const aliceExpectedETHGain = liquidatedColl.mul(toBN(dec(15000, 18))).div(toBN(dec(200000, 18))) - const aliceETHGain = await stabilityPool.getDepositorETHGain(alice) - assert.isTrue(aliceExpectedETHGain.eq(aliceETHGain)) - - // Alice retrieves all of her deposit - await stabilityPool.withdrawFromSP(dec(15000, 18), { from: alice }) - - const active_ETH_After = await activePool.getETH() - const stability_ETH_After = await stabilityPool.getETH() - - const active_ETH_Difference = (active_ETH_Before.sub(active_ETH_After)) - const stability_ETH_Difference = (stability_ETH_Before.sub(stability_ETH_After)) - - assert.equal(active_ETH_Difference, '0') - - // Expect StabilityPool to have decreased by Alice's ETHGain - assert.isAtMost(th.getDifference(stability_ETH_Difference, aliceETHGain), 10000) - }) - - it("withdrawFromSP(): All depositors are able to withdraw from the SP to their account", async () => { - // Whale opens trove - await openTrove({ ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // 1 defaulter open - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // 6 Accounts open troves and provide to SP - const depositors = [alice, bob, carol, dennis, erin, flyn] - for (account of depositors) { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: account } }) - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: account }) - } - - await priceFeed.setPrice(dec(105, 18)) - await troveManager.liquidate(defaulter_1) - - await priceFeed.setPrice(dec(200, 18)) - - // All depositors attempt to withdraw - await stabilityPool.withdrawFromSP(dec(10000, 18), { from: alice }) - assert.equal(((await stabilityPool.deposits(alice))[0]).toString(), '0') - await stabilityPool.withdrawFromSP(dec(10000, 18), { from: bob }) - assert.equal(((await stabilityPool.deposits(alice))[0]).toString(), '0') - await stabilityPool.withdrawFromSP(dec(10000, 18), { from: carol }) - assert.equal(((await stabilityPool.deposits(alice))[0]).toString(), '0') - await stabilityPool.withdrawFromSP(dec(10000, 18), { from: dennis }) - assert.equal(((await stabilityPool.deposits(alice))[0]).toString(), '0') - await stabilityPool.withdrawFromSP(dec(10000, 18), { from: erin }) - assert.equal(((await stabilityPool.deposits(alice))[0]).toString(), '0') - await stabilityPool.withdrawFromSP(dec(10000, 18), { from: flyn }) - assert.equal(((await stabilityPool.deposits(alice))[0]).toString(), '0') - - const totalDeposits = (await stabilityPool.getTotalLUSDDeposits()).toString() - - assert.isAtMost(th.getDifference(totalDeposits, '0'), 100000) - }) - - it("withdrawFromSP(): increases depositor's LUSD token balance by the expected amount", async () => { - // Whale opens trove - await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // 1 defaulter opens trove - await borrowerOperations.openTrove(th._100pct, await getOpenTroveLUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }) - - const defaulterDebt = (await troveManager.getEntireDebtAndColl(defaulter_1))[0] - - // 6 Accounts open troves and provide to SP - const depositors = [alice, bob, carol, dennis, erin, flyn] - for (account of depositors) { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: account } }) - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: account }) - } - - await priceFeed.setPrice(dec(105, 18)) - await troveManager.liquidate(defaulter_1) - - const aliceBalBefore = await lusdToken.balanceOf(alice) - const bobBalBefore = await lusdToken.balanceOf(bob) - - /* From an offset of 10000 LUSD, each depositor receives - LUSDLoss = 1666.6666666666666666 LUSD - - and thus with a deposit of 10000 LUSD, each should withdraw 8333.3333333333333333 LUSD (in practice, slightly less due to rounding error) - */ - - // Price bounces back to $200 per ETH - await priceFeed.setPrice(dec(200, 18)) - - // Bob issues a further 5000 LUSD from his trove - await borrowerOperations.withdrawLUSD(th._100pct, dec(5000, 18), bob, bob, { from: bob }) - - // Expect Alice's LUSD balance increase be very close to 8333.3333333333333333 LUSD - await stabilityPool.withdrawFromSP(dec(10000, 18), { from: alice }) - const aliceBalance = (await lusdToken.balanceOf(alice)) - - assert.isAtMost(th.getDifference(aliceBalance.sub(aliceBalBefore), '8333333333333333333333'), 100000) - - // expect Bob's LUSD balance increase to be very close to 13333.33333333333333333 LUSD - await stabilityPool.withdrawFromSP(dec(10000, 18), { from: bob }) - const bobBalance = (await lusdToken.balanceOf(bob)) - assert.isAtMost(th.getDifference(bobBalance.sub(bobBalBefore), '13333333333333333333333'), 100000) - }) - - it("withdrawFromSP(): doesn't impact other users Stability deposits or ETH gains", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C open troves and make Stability Pool deposits - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) - - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: alice }) - await stabilityPool.provideToSP(dec(20000, 18), frontEnd_1, { from: bob }) - await stabilityPool.provideToSP(dec(30000, 18), frontEnd_1, { from: carol }) - - // Would-be defaulters open troves - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) - - // Price drops - await priceFeed.setPrice(dec(105, 18)) - - // Defaulters are liquidated - await troveManager.liquidate(defaulter_1) - await troveManager.liquidate(defaulter_2) - assert.isFalse(await sortedTroves.contains(defaulter_1)) - assert.isFalse(await sortedTroves.contains(defaulter_2)) - - const alice_LUSDDeposit_Before = (await stabilityPool.getCompoundedLUSDDeposit(alice)).toString() - const bob_LUSDDeposit_Before = (await stabilityPool.getCompoundedLUSDDeposit(bob)).toString() - - const alice_ETHGain_Before = (await stabilityPool.getDepositorETHGain(alice)).toString() - const bob_ETHGain_Before = (await stabilityPool.getDepositorETHGain(bob)).toString() - - //check non-zero LUSD and ETHGain in the Stability Pool - const LUSDinSP = await stabilityPool.getTotalLUSDDeposits() - const ETHinSP = await stabilityPool.getETH() - assert.isTrue(LUSDinSP.gt(mv._zeroBN)) - assert.isTrue(ETHinSP.gt(mv._zeroBN)) - - // Price rises - await priceFeed.setPrice(dec(200, 18)) - - // Carol withdraws her Stability deposit - assert.equal(((await stabilityPool.deposits(carol))[0]).toString(), dec(30000, 18)) - await stabilityPool.withdrawFromSP(dec(30000, 18), { from: carol }) - assert.equal(((await stabilityPool.deposits(carol))[0]).toString(), '0') - - const alice_LUSDDeposit_After = (await stabilityPool.getCompoundedLUSDDeposit(alice)).toString() - const bob_LUSDDeposit_After = (await stabilityPool.getCompoundedLUSDDeposit(bob)).toString() - - const alice_ETHGain_After = (await stabilityPool.getDepositorETHGain(alice)).toString() - const bob_ETHGain_After = (await stabilityPool.getDepositorETHGain(bob)).toString() - - // Check compounded deposits and ETH gains for A and B have not changed - assert.equal(alice_LUSDDeposit_Before, alice_LUSDDeposit_After) - assert.equal(bob_LUSDDeposit_Before, bob_LUSDDeposit_After) - - assert.equal(alice_ETHGain_Before, alice_ETHGain_After) - assert.equal(bob_ETHGain_Before, bob_ETHGain_After) - }) - - it("withdrawFromSP(): doesn't impact system debt, collateral or TCR ", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C open troves and make Stability Pool deposits - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) - - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: alice }) - await stabilityPool.provideToSP(dec(20000, 18), frontEnd_1, { from: bob }) - await stabilityPool.provideToSP(dec(30000, 18), frontEnd_1, { from: carol }) - - // Would-be defaulters open troves - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) - - // Price drops - await priceFeed.setPrice(dec(105, 18)) - - // Defaulters are liquidated - await troveManager.liquidate(defaulter_1) - await troveManager.liquidate(defaulter_2) - assert.isFalse(await sortedTroves.contains(defaulter_1)) - assert.isFalse(await sortedTroves.contains(defaulter_2)) - - // Price rises - await priceFeed.setPrice(dec(200, 18)) - - const activeDebt_Before = (await activePool.getLUSDDebt()).toString() - const defaultedDebt_Before = (await defaultPool.getLUSDDebt()).toString() - const activeColl_Before = (await activePool.getETH()).toString() - const defaultedColl_Before = (await defaultPool.getETH()).toString() - const TCR_Before = (await th.getTCR(contracts)).toString() - - // Carol withdraws her Stability deposit - assert.equal(((await stabilityPool.deposits(carol))[0]).toString(), dec(30000, 18)) - await stabilityPool.withdrawFromSP(dec(30000, 18), { from: carol }) - assert.equal(((await stabilityPool.deposits(carol))[0]).toString(), '0') - - const activeDebt_After = (await activePool.getLUSDDebt()).toString() - const defaultedDebt_After = (await defaultPool.getLUSDDebt()).toString() - const activeColl_After = (await activePool.getETH()).toString() - const defaultedColl_After = (await defaultPool.getETH()).toString() - const TCR_After = (await th.getTCR(contracts)).toString() - - // Check total system debt, collateral and TCR have not changed after a Stability deposit is made - assert.equal(activeDebt_Before, activeDebt_After) - assert.equal(defaultedDebt_Before, defaultedDebt_After) - assert.equal(activeColl_Before, activeColl_After) - assert.equal(defaultedColl_Before, defaultedColl_After) - assert.equal(TCR_Before, TCR_After) - }) - - it("withdrawFromSP(): doesn't impact any troves, including the caller's trove", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C open troves and make Stability Pool deposits - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) - - // A, B and C provide to SP - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: alice }) - await stabilityPool.provideToSP(dec(20000, 18), frontEnd_1, { from: bob }) - await stabilityPool.provideToSP(dec(30000, 18), frontEnd_1, { from: carol }) - - // Price drops - await priceFeed.setPrice(dec(105, 18)) - const price = await priceFeed.getPrice() - - // Get debt, collateral and ICR of all existing troves - const whale_Debt_Before = (await troveManager.Troves(whale))[0].toString() - const alice_Debt_Before = (await troveManager.Troves(alice))[0].toString() - const bob_Debt_Before = (await troveManager.Troves(bob))[0].toString() - const carol_Debt_Before = (await troveManager.Troves(carol))[0].toString() - - const whale_Coll_Before = (await troveManager.Troves(whale))[1].toString() - const alice_Coll_Before = (await troveManager.Troves(alice))[1].toString() - const bob_Coll_Before = (await troveManager.Troves(bob))[1].toString() - const carol_Coll_Before = (await troveManager.Troves(carol))[1].toString() - - const whale_ICR_Before = (await troveManager.getCurrentICR(whale, price)).toString() - const alice_ICR_Before = (await troveManager.getCurrentICR(alice, price)).toString() - const bob_ICR_Before = (await troveManager.getCurrentICR(bob, price)).toString() - const carol_ICR_Before = (await troveManager.getCurrentICR(carol, price)).toString() - - // price rises - await priceFeed.setPrice(dec(200, 18)) - - // Carol withdraws her Stability deposit - assert.equal(((await stabilityPool.deposits(carol))[0]).toString(), dec(30000, 18)) - await stabilityPool.withdrawFromSP(dec(30000, 18), { from: carol }) - assert.equal(((await stabilityPool.deposits(carol))[0]).toString(), '0') - - const whale_Debt_After = (await troveManager.Troves(whale))[0].toString() - const alice_Debt_After = (await troveManager.Troves(alice))[0].toString() - const bob_Debt_After = (await troveManager.Troves(bob))[0].toString() - const carol_Debt_After = (await troveManager.Troves(carol))[0].toString() - - const whale_Coll_After = (await troveManager.Troves(whale))[1].toString() - const alice_Coll_After = (await troveManager.Troves(alice))[1].toString() - const bob_Coll_After = (await troveManager.Troves(bob))[1].toString() - const carol_Coll_After = (await troveManager.Troves(carol))[1].toString() - - const whale_ICR_After = (await troveManager.getCurrentICR(whale, price)).toString() - const alice_ICR_After = (await troveManager.getCurrentICR(alice, price)).toString() - const bob_ICR_After = (await troveManager.getCurrentICR(bob, price)).toString() - const carol_ICR_After = (await troveManager.getCurrentICR(carol, price)).toString() - - // Check all troves are unaffected by Carol's Stability deposit withdrawal - assert.equal(whale_Debt_Before, whale_Debt_After) - assert.equal(alice_Debt_Before, alice_Debt_After) - assert.equal(bob_Debt_Before, bob_Debt_After) - assert.equal(carol_Debt_Before, carol_Debt_After) - - assert.equal(whale_Coll_Before, whale_Coll_After) - assert.equal(alice_Coll_Before, alice_Coll_After) - assert.equal(bob_Coll_Before, bob_Coll_After) - assert.equal(carol_Coll_Before, carol_Coll_After) - - assert.equal(whale_ICR_Before, whale_ICR_After) - assert.equal(alice_ICR_Before, alice_ICR_After) - assert.equal(bob_ICR_Before, bob_ICR_After) - assert.equal(carol_ICR_Before, carol_ICR_After) - }) - - it("withdrawFromSP(): succeeds when amount is 0 and system has an undercollateralized trove", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(100, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - - await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: A }) - - const A_initialDeposit = ((await stabilityPool.deposits(A))[0]).toString() - assert.equal(A_initialDeposit, dec(100, 18)) - - // defaulters opens trove - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) - - // ETH drops, defaulters are in liquidation range - await priceFeed.setPrice(dec(105, 18)) - const price = await priceFeed.getPrice() - assert.isTrue(await th.ICRbetween100and110(defaulter_1, troveManager, price)) - - await th.fastForwardTime(timeValues.MINUTES_IN_ONE_WEEK, web3.currentProvider) - - // Liquidate d1 - await troveManager.liquidate(defaulter_1) - assert.isFalse(await sortedTroves.contains(defaulter_1)) - - // Check d2 is undercollateralized - assert.isTrue(await th.ICRbetween100and110(defaulter_2, troveManager, price)) - assert.isTrue(await sortedTroves.contains(defaulter_2)) - - const A_ETHBalBefore = toBN(await web3.eth.getBalance(A)) - const A_LQTYBalBefore = await lqtyToken.balanceOf(A) - - // Check Alice has gains to withdraw - const A_pendingETHGain = await stabilityPool.getDepositorETHGain(A) - const A_pendingLQTYGain = await stabilityPool.getDepositorLQTYGain(A) - assert.isTrue(A_pendingETHGain.gt(toBN('0'))) - assert.isTrue(A_pendingLQTYGain.gt(toBN('0'))) - - // Check withdrawal of 0 succeeds - const tx = await stabilityPool.withdrawFromSP(0, { from: A, gasPrice: 0 }) - assert.isTrue(tx.receipt.status) - - const A_ETHBalAfter = toBN(await web3.eth.getBalance(A)) - - const A_LQTYBalAfter = await lqtyToken.balanceOf(A) - const A_LQTYBalDiff = A_LQTYBalAfter.sub(A_LQTYBalBefore) - - // Check A's ETH and LQTY balances have increased correctly - assert.isTrue(A_ETHBalAfter.sub(A_ETHBalBefore).eq(A_pendingETHGain)) - assert.isAtMost(th.getDifference(A_LQTYBalDiff, A_pendingLQTYGain), 1000) - }) - - it("withdrawFromSP(): withdrawing 0 LUSD doesn't alter the caller's deposit or the total LUSD in the Stability Pool", async () => { - // --- SETUP --- - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C open troves and make Stability Pool deposits - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) - - // A, B, C provides 100, 50, 30 LUSD to SP - await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: alice }) - await stabilityPool.provideToSP(dec(50, 18), frontEnd_1, { from: bob }) - await stabilityPool.provideToSP(dec(30, 18), frontEnd_1, { from: carol }) - - const bob_Deposit_Before = (await stabilityPool.getCompoundedLUSDDeposit(bob)).toString() - const LUSDinSP_Before = (await stabilityPool.getTotalLUSDDeposits()).toString() - - assert.equal(LUSDinSP_Before, dec(180, 18)) - - // Bob withdraws 0 LUSD from the Stability Pool - await stabilityPool.withdrawFromSP(0, { from: bob }) - - // check Bob's deposit and total LUSD in Stability Pool has not changed - const bob_Deposit_After = (await stabilityPool.getCompoundedLUSDDeposit(bob)).toString() - const LUSDinSP_After = (await stabilityPool.getTotalLUSDDeposits()).toString() - - assert.equal(bob_Deposit_Before, bob_Deposit_After) - assert.equal(LUSDinSP_Before, LUSDinSP_After) - }) - - it("withdrawFromSP(): withdrawing 0 ETH Gain does not alter the caller's ETH balance, their trove collateral, or the ETH in the Stability Pool", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C open troves and make Stability Pool deposits - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) - - // Would-be defaulter open trove - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // Price drops - await priceFeed.setPrice(dec(105, 18)) - - assert.isFalse(await th.checkRecoveryMode(contracts)) - - // Defaulter 1 liquidated, full offset - await troveManager.liquidate(defaulter_1) - - // Dennis opens trove and deposits to Stability Pool - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: dennis } }) - await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: dennis }) - - // Check Dennis has 0 ETHGain - const dennis_ETHGain = (await stabilityPool.getDepositorETHGain(dennis)).toString() - assert.equal(dennis_ETHGain, '0') - - const dennis_ETHBalance_Before = (web3.eth.getBalance(dennis)).toString() - const dennis_Collateral_Before = ((await troveManager.Troves(dennis))[1]).toString() - const ETHinSP_Before = (await stabilityPool.getETH()).toString() - - await priceFeed.setPrice(dec(200, 18)) - - // Dennis withdraws his full deposit and ETHGain to his account - await stabilityPool.withdrawFromSP(dec(100, 18), { from: dennis, gasPrice: 0 }) - - // Check withdrawal does not alter Dennis' ETH balance or his trove's collateral - const dennis_ETHBalance_After = (web3.eth.getBalance(dennis)).toString() - const dennis_Collateral_After = ((await troveManager.Troves(dennis))[1]).toString() - const ETHinSP_After = (await stabilityPool.getETH()).toString() - - assert.equal(dennis_ETHBalance_Before, dennis_ETHBalance_After) - assert.equal(dennis_Collateral_Before, dennis_Collateral_After) - - // Check withdrawal has not altered the ETH in the Stability Pool - assert.equal(ETHinSP_Before, ETHinSP_After) - }) - - it("withdrawFromSP(): Request to withdraw > caller's deposit only withdraws the caller's compounded deposit", async () => { - // --- SETUP --- - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C open troves and make Stability Pool deposits - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) - - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // A, B, C provide LUSD to SP - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: alice }) - await stabilityPool.provideToSP(dec(20000, 18), frontEnd_1, { from: bob }) - await stabilityPool.provideToSP(dec(30000, 18), frontEnd_1, { from: carol }) - - // Price drops - await priceFeed.setPrice(dec(105, 18)) - - // Liquidate defaulter 1 - await troveManager.liquidate(defaulter_1) - - const alice_LUSD_Balance_Before = await lusdToken.balanceOf(alice) - const bob_LUSD_Balance_Before = await lusdToken.balanceOf(bob) - - const alice_Deposit_Before = await stabilityPool.getCompoundedLUSDDeposit(alice) - const bob_Deposit_Before = await stabilityPool.getCompoundedLUSDDeposit(bob) - - const LUSDinSP_Before = await stabilityPool.getTotalLUSDDeposits() - - await priceFeed.setPrice(dec(200, 18)) - - // Bob attempts to withdraws 1 wei more than his compounded deposit from the Stability Pool - await stabilityPool.withdrawFromSP(bob_Deposit_Before.add(toBN(1)), { from: bob }) - - // Check Bob's LUSD balance has risen by only the value of his compounded deposit - const bob_expectedLUSDBalance = (bob_LUSD_Balance_Before.add(bob_Deposit_Before)).toString() - const bob_LUSD_Balance_After = (await lusdToken.balanceOf(bob)).toString() - assert.equal(bob_LUSD_Balance_After, bob_expectedLUSDBalance) - - // Alice attempts to withdraws 2309842309.000000000000000000 LUSD from the Stability Pool - await stabilityPool.withdrawFromSP('2309842309000000000000000000', { from: alice }) - - // Check Alice's LUSD balance has risen by only the value of her compounded deposit - const alice_expectedLUSDBalance = (alice_LUSD_Balance_Before.add(alice_Deposit_Before)).toString() - const alice_LUSD_Balance_After = (await lusdToken.balanceOf(alice)).toString() - assert.equal(alice_LUSD_Balance_After, alice_expectedLUSDBalance) - - // Check LUSD in Stability Pool has been reduced by only Alice's compounded deposit and Bob's compounded deposit - const expectedLUSDinSP = (LUSDinSP_Before.sub(alice_Deposit_Before).sub(bob_Deposit_Before)).toString() - const LUSDinSP_After = (await stabilityPool.getTotalLUSDDeposits()).toString() - assert.equal(LUSDinSP_After, expectedLUSDinSP) - }) - - it("withdrawFromSP(): Request to withdraw 2^256-1 LUSD only withdraws the caller's compounded deposit", async () => { - // --- SETUP --- - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C open troves - // A, B, C open troves - // A, B, C open troves - // A, B, C open troves - // A, B, C open troves - // A, B, C open troves - // A, B, C open troves - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) - - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // A, B, C provides 100, 50, 30 LUSD to SP - await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: alice }) - await stabilityPool.provideToSP(dec(50, 18), frontEnd_1, { from: bob }) - await stabilityPool.provideToSP(dec(30, 18), frontEnd_1, { from: carol }) - - // Price drops - await priceFeed.setPrice(dec(100, 18)) - - // Liquidate defaulter 1 - await troveManager.liquidate(defaulter_1) - - const bob_LUSD_Balance_Before = await lusdToken.balanceOf(bob) - - const bob_Deposit_Before = await stabilityPool.getCompoundedLUSDDeposit(bob) - - const LUSDinSP_Before = await stabilityPool.getTotalLUSDDeposits() - - const maxBytes32 = web3.utils.toBN("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") - - // Price drops - await priceFeed.setPrice(dec(200, 18)) - - // Bob attempts to withdraws maxBytes32 LUSD from the Stability Pool - await stabilityPool.withdrawFromSP(maxBytes32, { from: bob }) - - // Check Bob's LUSD balance has risen by only the value of his compounded deposit - const bob_expectedLUSDBalance = (bob_LUSD_Balance_Before.add(bob_Deposit_Before)).toString() - const bob_LUSD_Balance_After = (await lusdToken.balanceOf(bob)).toString() - assert.equal(bob_LUSD_Balance_After, bob_expectedLUSDBalance) - - // Check LUSD in Stability Pool has been reduced by only Bob's compounded deposit - const expectedLUSDinSP = (LUSDinSP_Before.sub(bob_Deposit_Before)).toString() - const LUSDinSP_After = (await stabilityPool.getTotalLUSDDeposits()).toString() - assert.equal(LUSDinSP_After, expectedLUSDinSP) - }) - - it("withdrawFromSP(): caller can withdraw full deposit and ETH gain during Recovery Mode", async () => { - // --- SETUP --- - - // Price doubles - await priceFeed.setPrice(dec(400, 18)) - await openTrove({ extraLUSDAmount: toBN(dec(1000000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale } }) - // Price halves - await priceFeed.setPrice(dec(200, 18)) - - // A, B, C open troves and make Stability Pool deposits - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(4, 18)), extraParams: { from: alice } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(4, 18)), extraParams: { from: bob } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(4, 18)), extraParams: { from: carol } }) - - await borrowerOperations.openTrove(th._100pct, await getOpenTroveLUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }) - - // A, B, C provides 10000, 5000, 3000 LUSD to SP - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: alice }) - await stabilityPool.provideToSP(dec(5000, 18), frontEnd_1, { from: bob }) - await stabilityPool.provideToSP(dec(3000, 18), frontEnd_1, { from: carol }) - - // Price drops - await priceFeed.setPrice(dec(105, 18)) - const price = await priceFeed.getPrice() - - assert.isTrue(await th.checkRecoveryMode(contracts)) - - // Liquidate defaulter 1 - await troveManager.liquidate(defaulter_1) - assert.isFalse(await sortedTroves.contains(defaulter_1)) - - const alice_LUSD_Balance_Before = await lusdToken.balanceOf(alice) - const bob_LUSD_Balance_Before = await lusdToken.balanceOf(bob) - const carol_LUSD_Balance_Before = await lusdToken.balanceOf(carol) - - const alice_ETH_Balance_Before = web3.utils.toBN(await web3.eth.getBalance(alice)) - const bob_ETH_Balance_Before = web3.utils.toBN(await web3.eth.getBalance(bob)) - const carol_ETH_Balance_Before = web3.utils.toBN(await web3.eth.getBalance(carol)) - - const alice_Deposit_Before = await stabilityPool.getCompoundedLUSDDeposit(alice) - const bob_Deposit_Before = await stabilityPool.getCompoundedLUSDDeposit(bob) - const carol_Deposit_Before = await stabilityPool.getCompoundedLUSDDeposit(carol) - - const alice_ETHGain_Before = await stabilityPool.getDepositorETHGain(alice) - const bob_ETHGain_Before = await stabilityPool.getDepositorETHGain(bob) - const carol_ETHGain_Before = await stabilityPool.getDepositorETHGain(carol) - - const LUSDinSP_Before = await stabilityPool.getTotalLUSDDeposits() - - // Price rises - await priceFeed.setPrice(dec(220, 18)) - - assert.isTrue(await th.checkRecoveryMode(contracts)) - - // A, B, C withdraw their full deposits from the Stability Pool - await stabilityPool.withdrawFromSP(dec(10000, 18), { from: alice, gasPrice: 0 }) - await stabilityPool.withdrawFromSP(dec(5000, 18), { from: bob, gasPrice: 0 }) - await stabilityPool.withdrawFromSP(dec(3000, 18), { from: carol, gasPrice: 0 }) - - // Check LUSD balances of A, B, C have risen by the value of their compounded deposits, respectively - const alice_expectedLUSDBalance = (alice_LUSD_Balance_Before.add(alice_Deposit_Before)).toString() - - const bob_expectedLUSDBalance = (bob_LUSD_Balance_Before.add(bob_Deposit_Before)).toString() - const carol_expectedLUSDBalance = (carol_LUSD_Balance_Before.add(carol_Deposit_Before)).toString() - - const alice_LUSD_Balance_After = (await lusdToken.balanceOf(alice)).toString() - - const bob_LUSD_Balance_After = (await lusdToken.balanceOf(bob)).toString() - const carol_LUSD_Balance_After = (await lusdToken.balanceOf(carol)).toString() - - assert.equal(alice_LUSD_Balance_After, alice_expectedLUSDBalance) - assert.equal(bob_LUSD_Balance_After, bob_expectedLUSDBalance) - assert.equal(carol_LUSD_Balance_After, carol_expectedLUSDBalance) - - // Check ETH balances of A, B, C have increased by the value of their ETH gain from liquidations, respectively - const alice_expectedETHBalance = (alice_ETH_Balance_Before.add(alice_ETHGain_Before)).toString() - const bob_expectedETHBalance = (bob_ETH_Balance_Before.add(bob_ETHGain_Before)).toString() - const carol_expectedETHBalance = (carol_ETH_Balance_Before.add(carol_ETHGain_Before)).toString() - - const alice_ETHBalance_After = (await web3.eth.getBalance(alice)).toString() - const bob_ETHBalance_After = (await web3.eth.getBalance(bob)).toString() - const carol_ETHBalance_After = (await web3.eth.getBalance(carol)).toString() - - assert.equal(alice_expectedETHBalance, alice_ETHBalance_After) - assert.equal(bob_expectedETHBalance, bob_ETHBalance_After) - assert.equal(carol_expectedETHBalance, carol_ETHBalance_After) - - // Check LUSD in Stability Pool has been reduced by A, B and C's compounded deposit - const expectedLUSDinSP = (LUSDinSP_Before - .sub(alice_Deposit_Before) - .sub(bob_Deposit_Before) - .sub(carol_Deposit_Before)) - .toString() - const LUSDinSP_After = (await stabilityPool.getTotalLUSDDeposits()).toString() - assert.equal(LUSDinSP_After, expectedLUSDinSP) - - // Check ETH in SP has reduced to zero - const ETHinSP_After = (await stabilityPool.getETH()).toString() - assert.isAtMost(th.getDifference(ETHinSP_After, '0'), 100000) - }) - - it("getDepositorETHGain(): depositor does not earn further ETH gains from liquidations while their compounded deposit == 0: ", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(1, 24)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C open troves - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) - - // defaulters open troves - await openTrove({ extraLUSDAmount: toBN(dec(15000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2 } }) - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_3 } }) - - // A, B, provide 10000, 5000 LUSD to SP - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: alice }) - await stabilityPool.provideToSP(dec(5000, 18), frontEnd_1, { from: bob }) - - //price drops - await priceFeed.setPrice(dec(105, 18)) - - // Liquidate defaulter 1. Empties the Pool - await troveManager.liquidate(defaulter_1) - assert.isFalse(await sortedTroves.contains(defaulter_1)) - - const LUSDinSP = (await stabilityPool.getTotalLUSDDeposits()).toString() - assert.equal(LUSDinSP, '0') - - // Check Stability deposits have been fully cancelled with debt, and are now all zero - const alice_Deposit = (await stabilityPool.getCompoundedLUSDDeposit(alice)).toString() - const bob_Deposit = (await stabilityPool.getCompoundedLUSDDeposit(bob)).toString() - - assert.equal(alice_Deposit, '0') - assert.equal(bob_Deposit, '0') - - // Get ETH gain for A and B - const alice_ETHGain_1 = (await stabilityPool.getDepositorETHGain(alice)).toString() - const bob_ETHGain_1 = (await stabilityPool.getDepositorETHGain(bob)).toString() - - // Whale deposits 10000 LUSD to Stability Pool - await stabilityPool.provideToSP(dec(1, 24), frontEnd_1, { from: whale }) - - // Liquidation 2 - await troveManager.liquidate(defaulter_2) - assert.isFalse(await sortedTroves.contains(defaulter_2)) - - // Check Alice and Bob have not received ETH gain from liquidation 2 while their deposit was 0 - const alice_ETHGain_2 = (await stabilityPool.getDepositorETHGain(alice)).toString() - const bob_ETHGain_2 = (await stabilityPool.getDepositorETHGain(bob)).toString() - - assert.equal(alice_ETHGain_1, alice_ETHGain_2) - assert.equal(bob_ETHGain_1, bob_ETHGain_2) - - // Liquidation 3 - await troveManager.liquidate(defaulter_3) - assert.isFalse(await sortedTroves.contains(defaulter_3)) - - // Check Alice and Bob have not received ETH gain from liquidation 3 while their deposit was 0 - const alice_ETHGain_3 = (await stabilityPool.getDepositorETHGain(alice)).toString() - const bob_ETHGain_3 = (await stabilityPool.getDepositorETHGain(bob)).toString() - - assert.equal(alice_ETHGain_1, alice_ETHGain_3) - assert.equal(bob_ETHGain_1, bob_ETHGain_3) - }) - - // --- LQTY functionality --- - it("withdrawFromSP(): triggers LQTY reward event - increases the sum G", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(1, 24)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C open troves - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - - // A and B provide to SP - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(10000, 18), ZERO_ADDRESS, { from: B }) - - const G_Before = await stabilityPool.epochToScaleToG(0, 0) - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // A withdraws from SP - await stabilityPool.withdrawFromSP(dec(5000, 18), { from: A }) - - const G_1 = await stabilityPool.epochToScaleToG(0, 0) - - // Expect G has increased from the LQTY reward event triggered - assert.isTrue(G_1.gt(G_Before)) - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // A withdraws from SP - await stabilityPool.withdrawFromSP(dec(5000, 18), { from: B }) - - const G_2 = await stabilityPool.epochToScaleToG(0, 0) - - // Expect G has increased from the LQTY reward event triggered - assert.isTrue(G_2.gt(G_1)) - }) - - it("withdrawFromSP(), partial withdrawal: doesn't change the front end tag", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // whale transfer to troves D and E - await lusdToken.transfer(D, dec(100, 18), { from: whale }) - await lusdToken.transfer(E, dec(200, 18), { from: whale }) - - // A, B, C open troves - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - - // A, B, C, D, E provide to SP - await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: B }) - await stabilityPool.provideToSP(dec(30, 18), ZERO_ADDRESS, { from: C }) - await stabilityPool.provideToSP(dec(40, 18), frontEnd_1, { from: D }) - await stabilityPool.provideToSP(dec(50, 18), ZERO_ADDRESS, { from: E }) - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // A, B, C, D, E withdraw, from different front ends - await stabilityPool.withdrawFromSP(dec(5, 18), { from: A }) - await stabilityPool.withdrawFromSP(dec(10, 18), { from: B }) - await stabilityPool.withdrawFromSP(dec(15, 18), { from: C }) - await stabilityPool.withdrawFromSP(dec(20, 18), { from: D }) - await stabilityPool.withdrawFromSP(dec(25, 18), { from: E }) - - const frontEndTag_A = (await stabilityPool.deposits(A))[1] - const frontEndTag_B = (await stabilityPool.deposits(B))[1] - const frontEndTag_C = (await stabilityPool.deposits(C))[1] - const frontEndTag_D = (await stabilityPool.deposits(D))[1] - const frontEndTag_E = (await stabilityPool.deposits(E))[1] - - // Check deposits are still tagged with their original front end - assert.equal(frontEndTag_A, frontEnd_1) - assert.equal(frontEndTag_B, frontEnd_2) - assert.equal(frontEndTag_C, ZERO_ADDRESS) - assert.equal(frontEndTag_D, frontEnd_1) - assert.equal(frontEndTag_E, ZERO_ADDRESS) - }) - - it("withdrawFromSP(), partial withdrawal: depositor receives LQTY rewards", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C open troves - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - - // A, B, C, provide to SP - await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: B }) - await stabilityPool.provideToSP(dec(30, 18), ZERO_ADDRESS, { from: C }) - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // Get A, B, C LQTY balance before - const A_LQTYBalance_Before = await lqtyToken.balanceOf(A) - const B_LQTYBalance_Before = await lqtyToken.balanceOf(B) - const C_LQTYBalance_Before = await lqtyToken.balanceOf(C) - - // A, B, C withdraw - await stabilityPool.withdrawFromSP(dec(1, 18), { from: A }) - await stabilityPool.withdrawFromSP(dec(2, 18), { from: B }) - await stabilityPool.withdrawFromSP(dec(3, 18), { from: C }) - - // Get LQTY balance after - const A_LQTYBalance_After = await lqtyToken.balanceOf(A) - const B_LQTYBalance_After = await lqtyToken.balanceOf(B) - const C_LQTYBalance_After = await lqtyToken.balanceOf(C) - - // Check LQTY Balance of A, B, C has increased - assert.isTrue(A_LQTYBalance_After.gt(A_LQTYBalance_Before)) - assert.isTrue(B_LQTYBalance_After.gt(B_LQTYBalance_Before)) - assert.isTrue(C_LQTYBalance_After.gt(C_LQTYBalance_Before)) - }) - - it("withdrawFromSP(), partial withdrawal: tagged front end receives LQTY rewards", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C open troves - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - - // A, B, C, provide to SP - await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: B }) - await stabilityPool.provideToSP(dec(30, 18), frontEnd_3, { from: C }) - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // Get front ends' LQTY balance before - const F1_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_1) - const F2_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_2) - const F3_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_3) - - // A, B, C withdraw - await stabilityPool.withdrawFromSP(dec(1, 18), { from: A }) - await stabilityPool.withdrawFromSP(dec(2, 18), { from: B }) - await stabilityPool.withdrawFromSP(dec(3, 18), { from: C }) - - // Get front ends' LQTY balance after - const F1_LQTYBalance_After = await lqtyToken.balanceOf(A) - const F2_LQTYBalance_After = await lqtyToken.balanceOf(B) - const F3_LQTYBalance_After = await lqtyToken.balanceOf(C) - - // Check LQTY Balance of front ends has increased - assert.isTrue(F1_LQTYBalance_After.gt(F1_LQTYBalance_Before)) - assert.isTrue(F2_LQTYBalance_After.gt(F2_LQTYBalance_Before)) - assert.isTrue(F3_LQTYBalance_After.gt(F3_LQTYBalance_Before)) - }) - - it("withdrawFromSP(), partial withdrawal: tagged front end's stake decreases", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C, D, E, F open troves - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: E } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: F } }) - - // A, B, C, D, E, F provide to SP - await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: B }) - await stabilityPool.provideToSP(dec(30, 18), frontEnd_3, { from: C }) - await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: D }) - await stabilityPool.provideToSP(dec(20, 18), frontEnd_2, { from: E }) - await stabilityPool.provideToSP(dec(30, 18), frontEnd_3, { from: F }) - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // Get front ends' stake before - const F1_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_1) - const F2_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_2) - const F3_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_3) - - // A, B, C withdraw - await stabilityPool.withdrawFromSP(dec(1, 18), { from: A }) - await stabilityPool.withdrawFromSP(dec(2, 18), { from: B }) - await stabilityPool.withdrawFromSP(dec(3, 18), { from: C }) - - // Get front ends' stakes after - const F1_Stake_After = await stabilityPool.frontEndStakes(frontEnd_1) - const F2_Stake_After = await stabilityPool.frontEndStakes(frontEnd_2) - const F3_Stake_After = await stabilityPool.frontEndStakes(frontEnd_3) - - // Check front ends' stakes have decreased - assert.isTrue(F1_Stake_After.lt(F1_Stake_Before)) - assert.isTrue(F2_Stake_After.lt(F2_Stake_Before)) - assert.isTrue(F3_Stake_After.lt(F3_Stake_Before)) - }) - - it("withdrawFromSP(), partial withdrawal: tagged front end's snapshots update", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C, open troves - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(40000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(60000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - - // D opens trove - await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) - - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // --- SETUP --- - - const deposit_A = dec(10000, 18) - const deposit_B = dec(20000, 18) - const deposit_C = dec(30000, 18) - - // A, B, C make their initial deposits - await stabilityPool.provideToSP(deposit_A, frontEnd_1, { from: A }) - await stabilityPool.provideToSP(deposit_B, frontEnd_2, { from: B }) - await stabilityPool.provideToSP(deposit_C, frontEnd_3, { from: C }) - - // fastforward time then make an SP deposit, to make G > 0 - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - await stabilityPool.provideToSP(dec(1000, 18), ZERO_ADDRESS, { from: D }) - - // perform a liquidation to make 0 < P < 1, and S > 0 - await priceFeed.setPrice(dec(105, 18)) - assert.isFalse(await th.checkRecoveryMode(contracts)) - - await troveManager.liquidate(defaulter_1) - - const currentEpoch = await stabilityPool.currentEpoch() - const currentScale = await stabilityPool.currentScale() - - const S_Before = await stabilityPool.epochToScaleToSum(currentEpoch, currentScale) - const P_Before = await stabilityPool.P() - const G_Before = await stabilityPool.epochToScaleToG(currentEpoch, currentScale) - - // Confirm 0 < P < 1 - assert.isTrue(P_Before.gt(toBN('0')) && P_Before.lt(toBN(dec(1, 18)))) - // Confirm S, G are both > 0 - assert.isTrue(S_Before.gt(toBN('0'))) - assert.isTrue(G_Before.gt(toBN('0'))) - - // Get front ends' snapshots before - for (frontEnd of [frontEnd_1, frontEnd_2, frontEnd_3]) { - const snapshot = await stabilityPool.frontEndSnapshots(frontEnd) - - assert.equal(snapshot[0], '0') // S (should always be 0 for front ends, since S corresponds to ETH gain) - assert.equal(snapshot[1], dec(1, 18)) // P - assert.equal(snapshot[2], '0') // G - assert.equal(snapshot[3], '0') // scale - assert.equal(snapshot[4], '0') // epoch - } - - // --- TEST --- - - await priceFeed.setPrice(dec(200, 18)) - - // A, B, C top withdraw part of their deposits. Grab G at each stage, as it can increase a bit - // between topups, because some block.timestamp time passes (and LQTY is issued) between ops - const G1 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) - await stabilityPool.withdrawFromSP(dec(1, 18), { from: A }) - - const G2 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) - await stabilityPool.withdrawFromSP(dec(2, 18), { from: B }) - - const G3 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) - await stabilityPool.withdrawFromSP(dec(3, 18), { from: C }) - - const frontEnds = [frontEnd_1, frontEnd_2, frontEnd_3] - const G_Values = [G1, G2, G3] - - // Map frontEnds to the value of G at time the deposit was made - frontEndToG = th.zipToObject(frontEnds, G_Values) - - // Get front ends' snapshots after - for (const [frontEnd, G] of Object.entries(frontEndToG)) { - const snapshot = await stabilityPool.frontEndSnapshots(frontEnd) - - // Check snapshots are the expected values - assert.equal(snapshot[0], '0') // S (should always be 0 for front ends) - assert.isTrue(snapshot[1].eq(P_Before)) // P - assert.isTrue(snapshot[2].eq(G)) // G - assert.equal(snapshot[3], '0') // scale - assert.equal(snapshot[4], '0') // epoch - } - }) - - it("withdrawFromSP(), full withdrawal: removes deposit's front end tag", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // Whale transfers to A, B - await lusdToken.transfer(A, dec(10000, 18), { from: whale }) - await lusdToken.transfer(B, dec(20000, 18), { from: whale }) - - //C, D open troves - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - await openTrove({ extraLUSDAmount: toBN(dec(40000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) - - // A, B, C, D make their initial deposits - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(20000, 18), ZERO_ADDRESS, { from: B }) - await stabilityPool.provideToSP(dec(30000, 18), frontEnd_2, { from: C }) - await stabilityPool.provideToSP(dec(40000, 18), ZERO_ADDRESS, { from: D }) - - // Check deposits are tagged with correct front end - const A_tagBefore = await getFrontEndTag(stabilityPool, A) - const B_tagBefore = await getFrontEndTag(stabilityPool, B) - const C_tagBefore = await getFrontEndTag(stabilityPool, C) - const D_tagBefore = await getFrontEndTag(stabilityPool, D) - - assert.equal(A_tagBefore, frontEnd_1) - assert.equal(B_tagBefore, ZERO_ADDRESS) - assert.equal(C_tagBefore, frontEnd_2) - assert.equal(D_tagBefore, ZERO_ADDRESS) - - // All depositors make full withdrawal - await stabilityPool.withdrawFromSP(dec(10000, 18), { from: A }) - await stabilityPool.withdrawFromSP(dec(20000, 18), { from: B }) - await stabilityPool.withdrawFromSP(dec(30000, 18), { from: C }) - await stabilityPool.withdrawFromSP(dec(40000, 18), { from: D }) - - // Check all deposits now have no front end tag - const A_tagAfter = await getFrontEndTag(stabilityPool, A) - const B_tagAfter = await getFrontEndTag(stabilityPool, B) - const C_tagAfter = await getFrontEndTag(stabilityPool, C) - const D_tagAfter = await getFrontEndTag(stabilityPool, D) - - assert.equal(A_tagAfter, ZERO_ADDRESS) - assert.equal(B_tagAfter, ZERO_ADDRESS) - assert.equal(C_tagAfter, ZERO_ADDRESS) - assert.equal(D_tagAfter, ZERO_ADDRESS) - }) - - it("withdrawFromSP(), full withdrawal: zero's depositor's snapshots", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(1000000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // SETUP: Execute a series of operations to make G, S > 0 and P < 1 - - // E opens trove and makes a deposit - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: E } }) - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_3, { from: E }) - - // Fast-forward time and make a second deposit, to trigger LQTY reward and make G > 0 - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_3, { from: E }) - - // perform a liquidation to make 0 < P < 1, and S > 0 - await priceFeed.setPrice(dec(105, 18)) - assert.isFalse(await th.checkRecoveryMode(contracts)) - - await troveManager.liquidate(defaulter_1) - - const currentEpoch = await stabilityPool.currentEpoch() - const currentScale = await stabilityPool.currentScale() - - const S_Before = await stabilityPool.epochToScaleToSum(currentEpoch, currentScale) - const P_Before = await stabilityPool.P() - const G_Before = await stabilityPool.epochToScaleToG(currentEpoch, currentScale) - - // Confirm 0 < P < 1 - assert.isTrue(P_Before.gt(toBN('0')) && P_Before.lt(toBN(dec(1, 18)))) - // Confirm S, G are both > 0 - assert.isTrue(S_Before.gt(toBN('0'))) - assert.isTrue(G_Before.gt(toBN('0'))) - - // --- TEST --- - - // Whale transfers to A, B - await lusdToken.transfer(A, dec(10000, 18), { from: whale }) - await lusdToken.transfer(B, dec(20000, 18), { from: whale }) - - await priceFeed.setPrice(dec(200, 18)) - - // C, D open troves - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: C } }) - await openTrove({ extraLUSDAmount: toBN(dec(40000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: D } }) - - // A, B, C, D make their initial deposits - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(20000, 18), ZERO_ADDRESS, { from: B }) - await stabilityPool.provideToSP(dec(30000, 18), frontEnd_2, { from: C }) - await stabilityPool.provideToSP(dec(40000, 18), ZERO_ADDRESS, { from: D }) - - // Check deposits snapshots are non-zero - - for (depositor of [A, B, C, D]) { - const snapshot = await stabilityPool.depositSnapshots(depositor) - - const ZERO = toBN('0') - // Check S,P, G snapshots are non-zero - assert.isTrue(snapshot[0].eq(S_Before)) // S - assert.isTrue(snapshot[1].eq(P_Before)) // P - assert.isTrue(snapshot[2].gt(ZERO)) // GL increases a bit between each depositor op, so just check it is non-zero - assert.equal(snapshot[3], '0') // scale - assert.equal(snapshot[4], '0') // epoch - } - - // All depositors make full withdrawal - await stabilityPool.withdrawFromSP(dec(10000, 18), { from: A }) - await stabilityPool.withdrawFromSP(dec(20000, 18), { from: B }) - await stabilityPool.withdrawFromSP(dec(30000, 18), { from: C }) - await stabilityPool.withdrawFromSP(dec(40000, 18), { from: D }) - - // Check all depositors' snapshots have been zero'd - for (depositor of [A, B, C, D]) { - const snapshot = await stabilityPool.depositSnapshots(depositor) - - // Check S, P, G snapshots are now zero - assert.equal(snapshot[0], '0') // S - assert.equal(snapshot[1], '0') // P - assert.equal(snapshot[2], '0') // G - assert.equal(snapshot[3], '0') // scale - assert.equal(snapshot[4], '0') // epoch - } - }) - - it("withdrawFromSP(), full withdrawal that reduces front end stake to 0: zero’s the front end’s snapshots", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // SETUP: Execute a series of operations to make G, S > 0 and P < 1 - - // E opens trove and makes a deposit - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: E } }) - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_3, { from: E }) - - // Fast-forward time and make a second deposit, to trigger LQTY reward and make G > 0 - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_3, { from: E }) - - // perform a liquidation to make 0 < P < 1, and S > 0 - await priceFeed.setPrice(dec(105, 18)) - assert.isFalse(await th.checkRecoveryMode(contracts)) - - await troveManager.liquidate(defaulter_1) - - const currentEpoch = await stabilityPool.currentEpoch() - const currentScale = await stabilityPool.currentScale() - - const S_Before = await stabilityPool.epochToScaleToSum(currentEpoch, currentScale) - const P_Before = await stabilityPool.P() - const G_Before = await stabilityPool.epochToScaleToG(currentEpoch, currentScale) - - // Confirm 0 < P < 1 - assert.isTrue(P_Before.gt(toBN('0')) && P_Before.lt(toBN(dec(1, 18)))) - // Confirm S, G are both > 0 - assert.isTrue(S_Before.gt(toBN('0'))) - assert.isTrue(G_Before.gt(toBN('0'))) - - // --- TEST --- - - // A, B open troves - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(40000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - - // A, B, make their initial deposits - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(20000, 18), frontEnd_2, { from: B }) - - // Check frontend snapshots are non-zero - for (frontEnd of [frontEnd_1, frontEnd_2]) { - const snapshot = await stabilityPool.frontEndSnapshots(frontEnd) - - const ZERO = toBN('0') - // Check S,P, G snapshots are non-zero - assert.equal(snapshot[0], '0') // S (always zero for front-end) - assert.isTrue(snapshot[1].eq(P_Before)) // P - assert.isTrue(snapshot[2].gt(ZERO)) // GL increases a bit between each depositor op, so just check it is non-zero - assert.equal(snapshot[3], '0') // scale - assert.equal(snapshot[4], '0') // epoch - } - - await priceFeed.setPrice(dec(200, 18)) - - // All depositors make full withdrawal - await stabilityPool.withdrawFromSP(dec(10000, 18), { from: A }) - await stabilityPool.withdrawFromSP(dec(20000, 18), { from: B }) - - // Check all front ends' snapshots have been zero'd - for (frontEnd of [frontEnd_1, frontEnd_2]) { - const snapshot = await stabilityPool.frontEndSnapshots(frontEnd) - - // Check S, P, G snapshots are now zero - assert.equal(snapshot[0], '0') // S (always zero for front-end) - assert.equal(snapshot[1], '0') // P - assert.equal(snapshot[2], '0') // G - assert.equal(snapshot[3], '0') // scale - assert.equal(snapshot[4], '0') // epoch - } - }) - - it("withdrawFromSP(), reverts when initial deposit value is 0", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A opens trove and join the Stability Pool - await openTrove({ extraLUSDAmount: toBN(dec(10100, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: A }) - - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // SETUP: Execute a series of operations to trigger LQTY and ETH rewards for depositor A - - // Fast-forward time and make a second deposit, to trigger LQTY reward and make G > 0 - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - await stabilityPool.provideToSP(dec(100, 18), frontEnd_1, { from: A }) - - // perform a liquidation to make 0 < P < 1, and S > 0 - await priceFeed.setPrice(dec(105, 18)) - assert.isFalse(await th.checkRecoveryMode(contracts)) - - await troveManager.liquidate(defaulter_1) - assert.isFalse(await sortedTroves.contains(defaulter_1)) - - await priceFeed.setPrice(dec(200, 18)) - - // A successfully withraws deposit and all gains - await stabilityPool.withdrawFromSP(dec(10100, 18), { from: A }) - - // Confirm A's recorded deposit is 0 - const A_deposit = (await stabilityPool.deposits(A))[0] // get initialValue property on deposit struct - assert.equal(A_deposit, '0') - - // --- TEST --- - const expectedRevertMessage = "StabilityPool: User must have a non-zero deposit" - - // Further withdrawal attempt from A - const withdrawalPromise_A = stabilityPool.withdrawFromSP(dec(10000, 18), { from: A }) - await th.assertRevert(withdrawalPromise_A, expectedRevertMessage) - - // Withdrawal attempt of a non-existent deposit, from C - const withdrawalPromise_C = stabilityPool.withdrawFromSP(dec(10000, 18), { from: C }) - await th.assertRevert(withdrawalPromise_C, expectedRevertMessage) - }) - - // --- withdrawETHGainToTrove --- - - it("withdrawETHGainToTrove(): reverts when user has no active deposit", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) - - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: alice }) - - const alice_initialDeposit = ((await stabilityPool.deposits(alice))[0]).toString() - const bob_initialDeposit = ((await stabilityPool.deposits(bob))[0]).toString() - - assert.equal(alice_initialDeposit, dec(10000, 18)) - assert.equal(bob_initialDeposit, '0') - - // Defaulter opens a trove, price drops, defaulter gets liquidated - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - await priceFeed.setPrice(dec(105, 18)) - assert.isFalse(await th.checkRecoveryMode(contracts)) - await troveManager.liquidate(defaulter_1) - assert.isFalse(await sortedTroves.contains(defaulter_1)) - - const txAlice = await stabilityPool.withdrawETHGainToTrove(alice, alice, { from: alice }) - assert.isTrue(txAlice.receipt.status) - - const txPromise_B = stabilityPool.withdrawETHGainToTrove(bob, bob, { from: bob }) - await th.assertRevert(txPromise_B) - }) - - it("withdrawETHGainToTrove(): Applies LUSDLoss to user's deposit, and redirects ETH reward to user's Trove", async () => { - // --- SETUP --- - // Whale deposits 185000 LUSD in StabilityPool - await openTrove({ extraLUSDAmount: toBN(dec(1000000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - await stabilityPool.provideToSP(dec(185000, 18), frontEnd_1, { from: whale }) - - // Defaulter opens trove - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // --- TEST --- - - // Alice makes deposit #1: 15000 LUSD - await openTrove({ extraLUSDAmount: toBN(dec(15000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: alice } }) - await stabilityPool.provideToSP(dec(15000, 18), frontEnd_1, { from: alice }) - - // check Alice's Trove recorded ETH Before: - const aliceTrove_Before = await troveManager.Troves(alice) - const aliceTrove_ETH_Before = aliceTrove_Before[1] - assert.isTrue(aliceTrove_ETH_Before.gt(toBN('0'))) - - // price drops: defaulter's Trove falls below MCR, alice and whale Trove remain active - await priceFeed.setPrice(dec(105, 18)); - - // Defaulter's Trove is closed - const liquidationTx_1 = await troveManager.liquidate(defaulter_1, { from: owner }) - const [liquidatedDebt, liquidatedColl, ,] = th.getEmittedLiquidationValues(liquidationTx_1) - - const ETHGain_A = await stabilityPool.getDepositorETHGain(alice) - const compoundedDeposit_A = await stabilityPool.getCompoundedLUSDDeposit(alice) - - // Alice should receive rewards proportional to her deposit as share of total deposits - const expectedETHGain_A = liquidatedColl.mul(toBN(dec(15000, 18))).div(toBN(dec(200000, 18))) - const expectedLUSDLoss_A = liquidatedDebt.mul(toBN(dec(15000, 18))).div(toBN(dec(200000, 18))) - const expectedCompoundedDeposit_A = toBN(dec(15000, 18)).sub(expectedLUSDLoss_A) - - assert.isAtMost(th.getDifference(expectedCompoundedDeposit_A, compoundedDeposit_A), 100000) - - // Alice sends her ETH Gains to her Trove - await stabilityPool.withdrawETHGainToTrove(alice, alice, { from: alice }) - - // check Alice's LUSDLoss has been applied to her deposit expectedCompoundedDeposit_A - alice_deposit_afterDefault = ((await stabilityPool.deposits(alice))[0]) - assert.isAtMost(th.getDifference(alice_deposit_afterDefault, expectedCompoundedDeposit_A), 100000) - - // check alice's Trove recorded ETH has increased by the expected reward amount - const aliceTrove_After = await troveManager.Troves(alice) - const aliceTrove_ETH_After = aliceTrove_After[1] - - const Trove_ETH_Increase = (aliceTrove_ETH_After.sub(aliceTrove_ETH_Before)).toString() - - assert.equal(Trove_ETH_Increase, ETHGain_A) - }) - - it("withdrawETHGainToTrove(): reverts if it would leave trove with ICR < MCR", async () => { - // --- SETUP --- - // Whale deposits 1850 LUSD in StabilityPool - await openTrove({ extraLUSDAmount: toBN(dec(1000000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - await stabilityPool.provideToSP(dec(185000, 18), frontEnd_1, { from: whale }) - - // defaulter opened - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // --- TEST --- - - // Alice makes deposit #1: 15000 LUSD - await openTrove({ extraLUSDAmount: toBN(dec(15000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) - await stabilityPool.provideToSP(dec(15000, 18), frontEnd_1, { from: alice }) - - // check alice's Trove recorded ETH Before: - const aliceTrove_Before = await troveManager.Troves(alice) - const aliceTrove_ETH_Before = aliceTrove_Before[1] - assert.isTrue(aliceTrove_ETH_Before.gt(toBN('0'))) - - // price drops: defaulter's Trove falls below MCR - await priceFeed.setPrice(dec(10, 18)); - - // defaulter's Trove is closed. - await troveManager.liquidate(defaulter_1, { from: owner }) - - // Alice attempts to her ETH Gains to her Trove - await assertRevert(stabilityPool.withdrawETHGainToTrove(alice, alice, { from: alice }), - "BorrowerOps: An operation that would result in ICR < MCR is not permitted") - }) - - it("withdrawETHGainToTrove(): Subsequent deposit and withdrawal attempt from same account, with no intermediate liquidations, withdraws zero ETH", async () => { - // --- SETUP --- - // Whale deposits 1850 LUSD in StabilityPool - await openTrove({ extraLUSDAmount: toBN(dec(1000000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - await stabilityPool.provideToSP(dec(185000, 18), frontEnd_1, { from: whale }) - - // defaulter opened - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // --- TEST --- - - // Alice makes deposit #1: 15000 LUSD - await openTrove({ extraLUSDAmount: toBN(dec(15000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) - await stabilityPool.provideToSP(dec(15000, 18), frontEnd_1, { from: alice }) - - // check alice's Trove recorded ETH Before: - const aliceTrove_Before = await troveManager.Troves(alice) - const aliceTrove_ETH_Before = aliceTrove_Before[1] - assert.isTrue(aliceTrove_ETH_Before.gt(toBN('0'))) - - // price drops: defaulter's Trove falls below MCR - await priceFeed.setPrice(dec(105, 18)); - - // defaulter's Trove is closed. - await troveManager.liquidate(defaulter_1, { from: owner }) - - // price bounces back - await priceFeed.setPrice(dec(200, 18)); - - // Alice sends her ETH Gains to her Trove - await stabilityPool.withdrawETHGainToTrove(alice, alice, { from: alice }) - - assert.equal(await stabilityPool.getDepositorETHGain(alice), 0) - - const ETHinSP_Before = (await stabilityPool.getETH()).toString() - - // Alice attempts second withdrawal from SP to Trove - reverts, due to 0 ETH Gain - const txPromise_A = stabilityPool.withdrawETHGainToTrove(alice, alice, { from: alice }) - await th.assertRevert(txPromise_A) - - // Check ETH in pool does not change - const ETHinSP_1 = (await stabilityPool.getETH()).toString() - assert.equal(ETHinSP_Before, ETHinSP_1) - - await priceFeed.setPrice(dec(200, 18)); - - // Alice attempts third withdrawal (this time, from SP to her own account) - await stabilityPool.withdrawFromSP(dec(15000, 18), { from: alice }) - - // Check ETH in pool does not change - const ETHinSP_2 = (await stabilityPool.getETH()).toString() - assert.equal(ETHinSP_Before, ETHinSP_2) - }) - - it("withdrawETHGainToTrove(): decreases StabilityPool ETH and increases activePool ETH", async () => { - // --- SETUP --- - // Whale deposits 185000 LUSD in StabilityPool - await openTrove({ extraLUSDAmount: toBN(dec(1000000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - await stabilityPool.provideToSP(dec(185000, 18), frontEnd_1, { from: whale }) - - // defaulter opened - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // --- TEST --- - - // Alice makes deposit #1: 15000 LUSD - await openTrove({ extraLUSDAmount: toBN(dec(15000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) - await stabilityPool.provideToSP(dec(15000, 18), frontEnd_1, { from: alice }) - - // price drops: defaulter's Trove falls below MCR - await priceFeed.setPrice(dec(100, 18)); - - // defaulter's Trove is closed. - const liquidationTx = await troveManager.liquidate(defaulter_1) - const [liquidatedDebt, liquidatedColl, gasComp] = th.getEmittedLiquidationValues(liquidationTx) - - // Expect alice to be entitled to 15000/200000 of the liquidated coll - const aliceExpectedETHGain = liquidatedColl.mul(toBN(dec(15000, 18))).div(toBN(dec(200000, 18))) - const aliceETHGain = await stabilityPool.getDepositorETHGain(alice) - assert.isTrue(aliceExpectedETHGain.eq(aliceETHGain)) - - // price bounces back - await priceFeed.setPrice(dec(200, 18)); - - //check activePool and StabilityPool Ether before retrieval: - const active_ETH_Before = await activePool.getETH() - const stability_ETH_Before = await stabilityPool.getETH() - - // Alice retrieves redirects ETH gain to her Trove - await stabilityPool.withdrawETHGainToTrove(alice, alice, { from: alice }) - - const active_ETH_After = await activePool.getETH() - const stability_ETH_After = await stabilityPool.getETH() - - const active_ETH_Difference = (active_ETH_After.sub(active_ETH_Before)) // AP ETH should increase - const stability_ETH_Difference = (stability_ETH_Before.sub(stability_ETH_After)) // SP ETH should decrease - - // check Pool ETH values change by Alice's ETHGain, i.e 0.075 ETH - assert.isAtMost(th.getDifference(active_ETH_Difference, aliceETHGain), 10000) - assert.isAtMost(th.getDifference(stability_ETH_Difference, aliceETHGain), 10000) - }) - - it("withdrawETHGainToTrove(): All depositors are able to withdraw their ETH gain from the SP to their Trove", async () => { - // Whale opens trove - await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // Defaulter opens trove - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // 6 Accounts open troves and provide to SP - const depositors = [alice, bob, carol, dennis, erin, flyn] - for (account of depositors) { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: account } }) - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: account }) - } - - await priceFeed.setPrice(dec(105, 18)) - await troveManager.liquidate(defaulter_1) - - // price bounces back - await priceFeed.setPrice(dec(200, 18)); - - // All depositors attempt to withdraw - const tx1 = await stabilityPool.withdrawETHGainToTrove(alice, alice, { from: alice }) - assert.isTrue(tx1.receipt.status) - const tx2 = await stabilityPool.withdrawETHGainToTrove(bob, bob, { from: bob }) - assert.isTrue(tx1.receipt.status) - const tx3 = await stabilityPool.withdrawETHGainToTrove(carol, carol, { from: carol }) - assert.isTrue(tx1.receipt.status) - const tx4 = await stabilityPool.withdrawETHGainToTrove(dennis, dennis, { from: dennis }) - assert.isTrue(tx1.receipt.status) - const tx5 = await stabilityPool.withdrawETHGainToTrove(erin, erin, { from: erin }) - assert.isTrue(tx1.receipt.status) - const tx6 = await stabilityPool.withdrawETHGainToTrove(flyn, flyn, { from: flyn }) - assert.isTrue(tx1.receipt.status) - }) - - it("withdrawETHGainToTrove(): All depositors withdraw, each withdraw their correct ETH gain", async () => { - // Whale opens trove - await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // defaulter opened - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // 6 Accounts open troves and provide to SP - const depositors = [alice, bob, carol, dennis, erin, flyn] - for (account of depositors) { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: account } }) - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: account }) - } - const collBefore = (await troveManager.Troves(alice))[1] // all troves have same coll before - - await priceFeed.setPrice(dec(105, 18)) - const liquidationTx = await troveManager.liquidate(defaulter_1) - const [, liquidatedColl, ,] = th.getEmittedLiquidationValues(liquidationTx) - - - /* All depositors attempt to withdraw their ETH gain to their Trove. Each depositor - receives (liquidatedColl/ 6). - - Thus, expected new collateral for each depositor with 1 Ether in their trove originally, is - (1 + liquidatedColl/6) - */ - - const expectedCollGain= liquidatedColl.div(toBN('6')) - - await priceFeed.setPrice(dec(200, 18)) - - await stabilityPool.withdrawETHGainToTrove(alice, alice, { from: alice }) - const aliceCollAfter = (await troveManager.Troves(alice))[1] - assert.isAtMost(th.getDifference(aliceCollAfter.sub(collBefore), expectedCollGain), 10000) - - await stabilityPool.withdrawETHGainToTrove(bob, bob, { from: bob }) - const bobCollAfter = (await troveManager.Troves(bob))[1] - assert.isAtMost(th.getDifference(bobCollAfter.sub(collBefore), expectedCollGain), 10000) - - await stabilityPool.withdrawETHGainToTrove(carol, carol, { from: carol }) - const carolCollAfter = (await troveManager.Troves(carol))[1] - assert.isAtMost(th.getDifference(carolCollAfter.sub(collBefore), expectedCollGain), 10000) - - await stabilityPool.withdrawETHGainToTrove(dennis, dennis, { from: dennis }) - const dennisCollAfter = (await troveManager.Troves(dennis))[1] - assert.isAtMost(th.getDifference(dennisCollAfter.sub(collBefore), expectedCollGain), 10000) - - await stabilityPool.withdrawETHGainToTrove(erin, erin, { from: erin }) - const erinCollAfter = (await troveManager.Troves(erin))[1] - assert.isAtMost(th.getDifference(erinCollAfter.sub(collBefore), expectedCollGain), 10000) - - await stabilityPool.withdrawETHGainToTrove(flyn, flyn, { from: flyn }) - const flynCollAfter = (await troveManager.Troves(flyn))[1] - assert.isAtMost(th.getDifference(flynCollAfter.sub(collBefore), expectedCollGain), 10000) - }) - - it("withdrawETHGainToTrove(): caller can withdraw full deposit and ETH gain to their trove during Recovery Mode", async () => { - // --- SETUP --- - - // Defaulter opens - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // A, B, C open troves - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) - - // A, B, C provides 10000, 5000, 3000 LUSD to SP - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: alice }) - await stabilityPool.provideToSP(dec(5000, 18), frontEnd_1, { from: bob }) - await stabilityPool.provideToSP(dec(3000, 18), frontEnd_1, { from: carol }) - - assert.isFalse(await th.checkRecoveryMode(contracts)) - - // Price drops to 105, - await priceFeed.setPrice(dec(105, 18)) - const price = await priceFeed.getPrice() - - assert.isTrue(await th.checkRecoveryMode(contracts)) - - // Check defaulter 1 has ICR: 100% < ICR < 110%. - assert.isTrue(await th.ICRbetween100and110(defaulter_1, troveManager, price)) - - const alice_Collateral_Before = (await troveManager.Troves(alice))[1] - const bob_Collateral_Before = (await troveManager.Troves(bob))[1] - const carol_Collateral_Before = (await troveManager.Troves(carol))[1] - - // Liquidate defaulter 1 - assert.isTrue(await sortedTroves.contains(defaulter_1)) - await troveManager.liquidate(defaulter_1) - assert.isFalse(await sortedTroves.contains(defaulter_1)) - - const alice_ETHGain_Before = await stabilityPool.getDepositorETHGain(alice) - const bob_ETHGain_Before = await stabilityPool.getDepositorETHGain(bob) - const carol_ETHGain_Before = await stabilityPool.getDepositorETHGain(carol) - - // A, B, C withdraw their full ETH gain from the Stability Pool to their trove - await stabilityPool.withdrawETHGainToTrove(alice, alice, { from: alice }) - await stabilityPool.withdrawETHGainToTrove(bob, bob, { from: bob }) - await stabilityPool.withdrawETHGainToTrove(carol, carol, { from: carol }) - - // Check collateral of troves A, B, C has increased by the value of their ETH gain from liquidations, respectively - const alice_expectedCollateral = (alice_Collateral_Before.add(alice_ETHGain_Before)).toString() - const bob_expectedColalteral = (bob_Collateral_Before.add(bob_ETHGain_Before)).toString() - const carol_expectedCollateral = (carol_Collateral_Before.add(carol_ETHGain_Before)).toString() - - const alice_Collateral_After = (await troveManager.Troves(alice))[1] - const bob_Collateral_After = (await troveManager.Troves(bob))[1] - const carol_Collateral_After = (await troveManager.Troves(carol))[1] - - assert.equal(alice_expectedCollateral, alice_Collateral_After) - assert.equal(bob_expectedColalteral, bob_Collateral_After) - assert.equal(carol_expectedCollateral, carol_Collateral_After) - - // Check ETH in SP has reduced to zero - const ETHinSP_After = (await stabilityPool.getETH()).toString() - assert.isAtMost(th.getDifference(ETHinSP_After, '0'), 100000) - }) - - it("withdrawETHGainToTrove(): reverts if user has no trove", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C open troves - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: bob } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: carol } }) - - // Defaulter opens - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // A transfers LUSD to D - await lusdToken.transfer(dennis, dec(10000, 18), { from: alice }) - - // D deposits to Stability Pool - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: dennis }) - - //Price drops - await priceFeed.setPrice(dec(105, 18)) - - //Liquidate defaulter 1 - await troveManager.liquidate(defaulter_1) - assert.isFalse(await sortedTroves.contains(defaulter_1)) - - await priceFeed.setPrice(dec(200, 18)) - - // D attempts to withdraw his ETH gain to Trove - await th.assertRevert(stabilityPool.withdrawETHGainToTrove(dennis, dennis, { from: dennis }), "caller must have an active trove to withdraw ETHGain to") - }) - - it("withdrawETHGainToTrove(): triggers LQTY reward event - increases the sum G", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C open troves - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - - // A and B provide to SP - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(10000, 18), ZERO_ADDRESS, { from: B }) - - // Defaulter opens a trove, price drops, defaulter gets liquidated - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - await priceFeed.setPrice(dec(105, 18)) - assert.isFalse(await th.checkRecoveryMode(contracts)) - await troveManager.liquidate(defaulter_1) - assert.isFalse(await sortedTroves.contains(defaulter_1)) - - const G_Before = await stabilityPool.epochToScaleToG(0, 0) - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - await priceFeed.setPrice(dec(200, 18)) - - // A withdraws from SP - await stabilityPool.withdrawFromSP(dec(50, 18), { from: A }) - - const G_1 = await stabilityPool.epochToScaleToG(0, 0) - - // Expect G has increased from the LQTY reward event triggered - assert.isTrue(G_1.gt(G_Before)) - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // Check B has non-zero ETH gain - assert.isTrue((await stabilityPool.getDepositorETHGain(B)).gt(ZERO)) - - // B withdraws to trove - await stabilityPool.withdrawETHGainToTrove(B, B, { from: B }) - - const G_2 = await stabilityPool.epochToScaleToG(0, 0) - - // Expect G has increased from the LQTY reward event triggered - assert.isTrue(G_2.gt(G_1)) - }) - - it("withdrawETHGainToTrove(), partial withdrawal: doesn't change the front end tag", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C open troves - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - - // A, B, C, D, E provide to SP - await stabilityPool.provideToSP(dec(10000, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(20000, 18), frontEnd_2, { from: B }) - await stabilityPool.provideToSP(dec(30000, 18), ZERO_ADDRESS, { from: C }) - - // Defaulter opens a trove, price drops, defaulter gets liquidated - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - await priceFeed.setPrice(dec(105, 18)) - assert.isFalse(await th.checkRecoveryMode(contracts)) - await troveManager.liquidate(defaulter_1) - assert.isFalse(await sortedTroves.contains(defaulter_1)) - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // Check A, B, C have non-zero ETH gain - assert.isTrue((await stabilityPool.getDepositorETHGain(A)).gt(ZERO)) - assert.isTrue((await stabilityPool.getDepositorETHGain(B)).gt(ZERO)) - assert.isTrue((await stabilityPool.getDepositorETHGain(C)).gt(ZERO)) - - await priceFeed.setPrice(dec(200, 18)) - - // A, B, C withdraw to trove - await stabilityPool.withdrawETHGainToTrove(A, A, { from: A }) - await stabilityPool.withdrawETHGainToTrove(B, B, { from: B }) - await stabilityPool.withdrawETHGainToTrove(C, C, { from: C }) - - const frontEndTag_A = (await stabilityPool.deposits(A))[1] - const frontEndTag_B = (await stabilityPool.deposits(B))[1] - const frontEndTag_C = (await stabilityPool.deposits(C))[1] - - // Check deposits are still tagged with their original front end - assert.equal(frontEndTag_A, frontEnd_1) - assert.equal(frontEndTag_B, frontEnd_2) - assert.equal(frontEndTag_C, ZERO_ADDRESS) - }) - - it("withdrawETHGainToTrove(), eligible deposit: depositor receives LQTY rewards", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C open troves - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - - // A, B, C, provide to SP - await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(2000, 18), frontEnd_2, { from: B }) - await stabilityPool.provideToSP(dec(3000, 18), ZERO_ADDRESS, { from: C }) - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // Defaulter opens a trove, price drops, defaulter gets liquidated - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - await priceFeed.setPrice(dec(105, 18)) - assert.isFalse(await th.checkRecoveryMode(contracts)) - await troveManager.liquidate(defaulter_1) - assert.isFalse(await sortedTroves.contains(defaulter_1)) - - // Get A, B, C LQTY balance before - const A_LQTYBalance_Before = await lqtyToken.balanceOf(A) - const B_LQTYBalance_Before = await lqtyToken.balanceOf(B) - const C_LQTYBalance_Before = await lqtyToken.balanceOf(C) - - // Check A, B, C have non-zero ETH gain - assert.isTrue((await stabilityPool.getDepositorETHGain(A)).gt(ZERO)) - assert.isTrue((await stabilityPool.getDepositorETHGain(B)).gt(ZERO)) - assert.isTrue((await stabilityPool.getDepositorETHGain(C)).gt(ZERO)) - - await priceFeed.setPrice(dec(200, 18)) - - // A, B, C withdraw to trove - await stabilityPool.withdrawETHGainToTrove(A, A, { from: A }) - await stabilityPool.withdrawETHGainToTrove(B, B, { from: B }) - await stabilityPool.withdrawETHGainToTrove(C, C, { from: C }) - - // Get LQTY balance after - const A_LQTYBalance_After = await lqtyToken.balanceOf(A) - const B_LQTYBalance_After = await lqtyToken.balanceOf(B) - const C_LQTYBalance_After = await lqtyToken.balanceOf(C) - - // Check LQTY Balance of A, B, C has increased - assert.isTrue(A_LQTYBalance_After.gt(A_LQTYBalance_Before)) - assert.isTrue(B_LQTYBalance_After.gt(B_LQTYBalance_Before)) - assert.isTrue(C_LQTYBalance_After.gt(C_LQTYBalance_Before)) - }) - - it("withdrawETHGainToTrove(), eligible deposit: tagged front end receives LQTY rewards", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C open troves - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - - // A, B, C, provide to SP - await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(2000, 18), frontEnd_2, { from: B }) - await stabilityPool.provideToSP(dec(3000, 18), frontEnd_3, { from: C }) - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // Defaulter opens a trove, price drops, defaulter gets liquidated - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - await priceFeed.setPrice(dec(105, 18)) - assert.isFalse(await th.checkRecoveryMode(contracts)) - await troveManager.liquidate(defaulter_1) - assert.isFalse(await sortedTroves.contains(defaulter_1)) - - // Get front ends' LQTY balance before - const F1_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_1) - const F2_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_2) - const F3_LQTYBalance_Before = await lqtyToken.balanceOf(frontEnd_3) - - await priceFeed.setPrice(dec(200, 18)) - - // Check A, B, C have non-zero ETH gain - assert.isTrue((await stabilityPool.getDepositorETHGain(A)).gt(ZERO)) - assert.isTrue((await stabilityPool.getDepositorETHGain(B)).gt(ZERO)) - assert.isTrue((await stabilityPool.getDepositorETHGain(C)).gt(ZERO)) - - // A, B, C withdraw - await stabilityPool.withdrawETHGainToTrove(A, A, { from: A }) - await stabilityPool.withdrawETHGainToTrove(B, B, { from: B }) - await stabilityPool.withdrawETHGainToTrove(C, C, { from: C }) - - // Get front ends' LQTY balance after - const F1_LQTYBalance_After = await lqtyToken.balanceOf(frontEnd_1) - const F2_LQTYBalance_After = await lqtyToken.balanceOf(frontEnd_2) - const F3_LQTYBalance_After = await lqtyToken.balanceOf(frontEnd_3) - - // Check LQTY Balance of front ends has increased - assert.isTrue(F1_LQTYBalance_After.gt(F1_LQTYBalance_Before)) - assert.isTrue(F2_LQTYBalance_After.gt(F2_LQTYBalance_Before)) - assert.isTrue(F3_LQTYBalance_After.gt(F3_LQTYBalance_Before)) - }) - - it("withdrawETHGainToTrove(), eligible deposit: tagged front end's stake decreases", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C, D, E, F open troves - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: E } }) - await openTrove({ extraLUSDAmount: toBN(dec(30000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: F } }) - - // A, B, C, D, E, F provide to SP - await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(2000, 18), frontEnd_2, { from: B }) - await stabilityPool.provideToSP(dec(3000, 18), frontEnd_3, { from: C }) - await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: D }) - await stabilityPool.provideToSP(dec(2000, 18), frontEnd_2, { from: E }) - await stabilityPool.provideToSP(dec(3000, 18), frontEnd_3, { from: F }) - - // Defaulter opens a trove, price drops, defaulter gets liquidated - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - await priceFeed.setPrice(dec(105, 18)) - assert.isFalse(await th.checkRecoveryMode(contracts)) - await troveManager.liquidate(defaulter_1) - assert.isFalse(await sortedTroves.contains(defaulter_1)) - - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - // Get front ends' stake before - const F1_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_1) - const F2_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_2) - const F3_Stake_Before = await stabilityPool.frontEndStakes(frontEnd_3) - - await priceFeed.setPrice(dec(200, 18)) - - // Check A, B, C have non-zero ETH gain - assert.isTrue((await stabilityPool.getDepositorETHGain(A)).gt(ZERO)) - assert.isTrue((await stabilityPool.getDepositorETHGain(B)).gt(ZERO)) - assert.isTrue((await stabilityPool.getDepositorETHGain(C)).gt(ZERO)) - - // A, B, C withdraw to trove - await stabilityPool.withdrawETHGainToTrove(A, A, { from: A }) - await stabilityPool.withdrawETHGainToTrove(B, B, { from: B }) - await stabilityPool.withdrawETHGainToTrove(C, C, { from: C }) - - // Get front ends' stakes after - const F1_Stake_After = await stabilityPool.frontEndStakes(frontEnd_1) - const F2_Stake_After = await stabilityPool.frontEndStakes(frontEnd_2) - const F3_Stake_After = await stabilityPool.frontEndStakes(frontEnd_3) - - // Check front ends' stakes have decreased - assert.isTrue(F1_Stake_After.lt(F1_Stake_Before)) - assert.isTrue(F2_Stake_After.lt(F2_Stake_Before)) - assert.isTrue(F3_Stake_After.lt(F3_Stake_Before)) - }) - - it("withdrawETHGainToTrove(), eligible deposit: tagged front end's snapshots update", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // A, B, C, open troves - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(40000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) - await openTrove({ extraLUSDAmount: toBN(dec(60000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - - // D opens trove - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) - - await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1 } }) - - // --- SETUP --- - - const deposit_A = dec(100, 18) - const deposit_B = dec(200, 18) - const deposit_C = dec(300, 18) - - // A, B, C make their initial deposits - await stabilityPool.provideToSP(deposit_A, frontEnd_1, { from: A }) - await stabilityPool.provideToSP(deposit_B, frontEnd_2, { from: B }) - await stabilityPool.provideToSP(deposit_C, frontEnd_3, { from: C }) - - // fastforward time then make an SP deposit, to make G > 0 - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - - await stabilityPool.provideToSP(dec(10000, 18), ZERO_ADDRESS, { from: D }) - - // perform a liquidation to make 0 < P < 1, and S > 0 - await priceFeed.setPrice(dec(105, 18)) - assert.isFalse(await th.checkRecoveryMode(contracts)) - - await troveManager.liquidate(defaulter_1) - - const currentEpoch = await stabilityPool.currentEpoch() - const currentScale = await stabilityPool.currentScale() - - const S_Before = await stabilityPool.epochToScaleToSum(currentEpoch, currentScale) - const P_Before = await stabilityPool.P() - const G_Before = await stabilityPool.epochToScaleToG(currentEpoch, currentScale) - - // Confirm 0 < P < 1 - assert.isTrue(P_Before.gt(toBN('0')) && P_Before.lt(toBN(dec(1, 18)))) - // Confirm S, G are both > 0 - assert.isTrue(S_Before.gt(toBN('0'))) - assert.isTrue(G_Before.gt(toBN('0'))) - - // Get front ends' snapshots before - for (frontEnd of [frontEnd_1, frontEnd_2, frontEnd_3]) { - const snapshot = await stabilityPool.frontEndSnapshots(frontEnd) - - assert.equal(snapshot[0], '0') // S (should always be 0 for front ends, since S corresponds to ETH gain) - assert.equal(snapshot[1], dec(1, 18)) // P - assert.equal(snapshot[2], '0') // G - assert.equal(snapshot[3], '0') // scale - assert.equal(snapshot[4], '0') // epoch - } - - // --- TEST --- - - // Check A, B, C have non-zero ETH gain - assert.isTrue((await stabilityPool.getDepositorETHGain(A)).gt(ZERO)) - assert.isTrue((await stabilityPool.getDepositorETHGain(B)).gt(ZERO)) - assert.isTrue((await stabilityPool.getDepositorETHGain(C)).gt(ZERO)) - - await priceFeed.setPrice(dec(200, 18)) - - // A, B, C withdraw ETH gain to troves. Grab G at each stage, as it can increase a bit - // between topups, because some block.timestamp time passes (and LQTY is issued) between ops - const G1 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) - await stabilityPool.withdrawETHGainToTrove(A, A, { from: A }) - - const G2 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) - await stabilityPool.withdrawETHGainToTrove(B, B, { from: B }) - - const G3 = await stabilityPool.epochToScaleToG(currentScale, currentEpoch) - await stabilityPool.withdrawETHGainToTrove(C, C, { from: C }) - - const frontEnds = [frontEnd_1, frontEnd_2, frontEnd_3] - const G_Values = [G1, G2, G3] - - // Map frontEnds to the value of G at time the deposit was made - frontEndToG = th.zipToObject(frontEnds, G_Values) - - // Get front ends' snapshots after - for (const [frontEnd, G] of Object.entries(frontEndToG)) { - const snapshot = await stabilityPool.frontEndSnapshots(frontEnd) - - // Check snapshots are the expected values - assert.equal(snapshot[0], '0') // S (should always be 0 for front ends) - assert.isTrue(snapshot[1].eq(P_Before)) // P - assert.isTrue(snapshot[2].eq(G)) // G - assert.equal(snapshot[3], '0') // scale - assert.equal(snapshot[4], '0') // epoch - } - }) - - it("withdrawETHGainToTrove(): reverts when depositor has no ETH gain", async () => { - await openTrove({ extraLUSDAmount: toBN(dec(100000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) - - // Whale transfers LUSD to A, B - await lusdToken.transfer(A, dec(10000, 18), { from: whale }) - await lusdToken.transfer(B, dec(20000, 18), { from: whale }) - - // C, D open troves - await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - await openTrove({ extraLUSDAmount: toBN(dec(4000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) - - // A, B, C, D provide to SP - await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: A }) - await stabilityPool.provideToSP(dec(20, 18), ZERO_ADDRESS, { from: B }) - await stabilityPool.provideToSP(dec(30, 18), frontEnd_2, { from: C }) - await stabilityPool.provideToSP(dec(40, 18), ZERO_ADDRESS, { from: D }) - - // fastforward time, and E makes a deposit, creating LQTY rewards for all - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: E } }) - await stabilityPool.provideToSP(dec(3000, 18), ZERO_ADDRESS, { from: E }) - - // Confirm A, B, C have zero ETH gain - assert.equal(await stabilityPool.getDepositorETHGain(A), '0') - assert.equal(await stabilityPool.getDepositorETHGain(B), '0') - assert.equal(await stabilityPool.getDepositorETHGain(C), '0') - - // Check withdrawETHGainToTrove reverts for A, B, C - const txPromise_A = stabilityPool.withdrawETHGainToTrove(A, A, { from: A }) - const txPromise_B = stabilityPool.withdrawETHGainToTrove(B, B, { from: B }) - const txPromise_C = stabilityPool.withdrawETHGainToTrove(C, C, { from: C }) - const txPromise_D = stabilityPool.withdrawETHGainToTrove(D, D, { from: D }) - - await th.assertRevert(txPromise_A) - await th.assertRevert(txPromise_B) - await th.assertRevert(txPromise_C) - await th.assertRevert(txPromise_D) - }) - - it("registerFrontEnd(): registers the front end and chosen kickback rate", async () => { - const unregisteredFrontEnds = [A, B, C, D, E] - - for (const frontEnd of unregisteredFrontEnds) { - assert.isFalse((await stabilityPool.frontEnds(frontEnd))[1]) // check inactive - assert.equal((await stabilityPool.frontEnds(frontEnd))[0], '0') // check no chosen kickback rate - } - - await stabilityPool.registerFrontEnd(dec(1, 18), { from: A }) - await stabilityPool.registerFrontEnd('897789897897897', { from: B }) - await stabilityPool.registerFrontEnd('99990098', { from: C }) - await stabilityPool.registerFrontEnd('37', { from: D }) - await stabilityPool.registerFrontEnd('0', { from: E }) - - // Check front ends are registered as active, and have correct kickback rates - assert.isTrue((await stabilityPool.frontEnds(A))[1]) - assert.equal((await stabilityPool.frontEnds(A))[0], dec(1, 18)) - - assert.isTrue((await stabilityPool.frontEnds(B))[1]) - assert.equal((await stabilityPool.frontEnds(B))[0], '897789897897897') - - assert.isTrue((await stabilityPool.frontEnds(C))[1]) - assert.equal((await stabilityPool.frontEnds(C))[0], '99990098') - - assert.isTrue((await stabilityPool.frontEnds(D))[1]) - assert.equal((await stabilityPool.frontEnds(D))[0], '37') - - assert.isTrue((await stabilityPool.frontEnds(E))[1]) - assert.equal((await stabilityPool.frontEnds(E))[0], '0') - }) - - it("registerFrontEnd(): reverts if the front end is already registered", async () => { - - await stabilityPool.registerFrontEnd(dec(1, 18), { from: A }) - await stabilityPool.registerFrontEnd('897789897897897', { from: B }) - await stabilityPool.registerFrontEnd('99990098', { from: C }) - - const _2ndAttempt_A = stabilityPool.registerFrontEnd(dec(1, 18), { from: A }) - const _2ndAttempt_B = stabilityPool.registerFrontEnd('897789897897897', { from: B }) - const _2ndAttempt_C = stabilityPool.registerFrontEnd('99990098', { from: C }) - - await th.assertRevert(_2ndAttempt_A, "StabilityPool: must not already be a registered front end") - await th.assertRevert(_2ndAttempt_B, "StabilityPool: must not already be a registered front end") - await th.assertRevert(_2ndAttempt_C, "StabilityPool: must not already be a registered front end") - }) - - it("registerFrontEnd(): reverts if the kickback rate >1", async () => { - - const invalidKickbackTx_A = stabilityPool.registerFrontEnd(dec(1, 19), { from: A }) - const invalidKickbackTx_B = stabilityPool.registerFrontEnd('1000000000000000001', { from: A }) - const invalidKickbackTx_C = stabilityPool.registerFrontEnd(dec(23423, 45), { from: A }) - const invalidKickbackTx_D = stabilityPool.registerFrontEnd(maxBytes32, { from: A }) - - await th.assertRevert(invalidKickbackTx_A, "StabilityPool: Kickback rate must be in range [0,1]") - await th.assertRevert(invalidKickbackTx_B, "StabilityPool: Kickback rate must be in range [0,1]") - await th.assertRevert(invalidKickbackTx_C, "StabilityPool: Kickback rate must be in range [0,1]") - await th.assertRevert(invalidKickbackTx_D, "StabilityPool: Kickback rate must be in range [0,1]") }) - it("registerFrontEnd(): reverts if address has a non-zero deposit already", async () => { - // C, D, E open troves - await openTrove({ extraLUSDAmount: toBN(dec(10, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) - await openTrove({ extraLUSDAmount: toBN(dec(10, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) - await openTrove({ extraLUSDAmount: toBN(dec(10, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: E } }) - - // C, E provides to SP - await stabilityPool.provideToSP(dec(10, 18), frontEnd_1, { from: C }) - await stabilityPool.provideToSP(dec(10, 18), ZERO_ADDRESS, { from: E }) - - const txPromise_C = stabilityPool.registerFrontEnd(dec(1, 18), { from: C }) - const txPromise_E = stabilityPool.registerFrontEnd(dec(1, 18), { from: E }) - await th.assertRevert(txPromise_C, "StabilityPool: User must have no deposit") - await th.assertRevert(txPromise_E, "StabilityPool: User must have no deposit") - - // D, with no deposit, successfully registers a front end - const txD = await stabilityPool.registerFrontEnd(dec(1, 18), { from: D }) - assert.isTrue(txD.receipt.status) - }) + // tests: + // 1. complex lqty staking + // 2. complex share + // 3. basic share with liquidation + // 4. price that exceeds max discount + // 5. price that exceeds balance + // 6. set params + // 7. test with front end + // 8. formula }) }) - -contract('Reset chain state', async accounts => { }) From 02670c5eee50c0230e13394433280a6320b1d2e1 Mon Sep 17 00:00:00 2001 From: yaron velner Date: Sat, 26 Jun 2021 23:09:35 +0300 Subject: [PATCH 03/90] fuzzy share + staking --- .../contracts/test/B.Protocol/BAMMTest.js | 101 +++++++++++++++++- 1 file changed, 98 insertions(+), 3 deletions(-) diff --git a/packages/contracts/test/B.Protocol/BAMMTest.js b/packages/contracts/test/B.Protocol/BAMMTest.js index 683fdeabe..a2d4d4264 100644 --- a/packages/contracts/test/B.Protocol/BAMMTest.js +++ b/packages/contracts/test/B.Protocol/BAMMTest.js @@ -20,12 +20,14 @@ const getFrontEndTag = async (stabilityPool, depositor) => { return (await stabilityPool.deposits(depositor))[1] } -contract('StabilityPool', async accounts => { +contract('BAMM', async accounts => { const [owner, defaulter_1, defaulter_2, defaulter_3, whale, alice, bob, carol, dennis, erin, flyn, A, B, C, D, E, F, + u1, u2, u3, u4, u5, + v1, v2, v3, v4, v5, frontEnd_1, frontEnd_2, frontEnd_3, ] = accounts; @@ -255,6 +257,86 @@ contract('StabilityPool', async accounts => { assert.equal(D_LQTYBalance_After.add(E_LQTYBalance_After).toString(), F_LQTYBalance_After.toString()) }) + it("test share + LQTY fuzzy", async () => { + const ammUsers = [u1, u2, u3, u4, u5] + const userBalance = [0, 0, 0, 0, 0] + const nonAmmUsers = [v1, v2, v3, v4, v5] + + let totalDeposits = 0 + + // test almost equal + assert(almostTheSame(web3.utils.toWei("9999"), web3.utils.toWei("9999"))) + assert(! almostTheSame(web3.utils.toWei("9989"), web3.utils.toWei("9999"))) + + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + for(let i = 0 ; i < ammUsers.length ; i++) { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: ammUsers[i] } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: nonAmmUsers[i] } }) + + await lusdToken.approve(bamm.address, dec(1000000, 18), { from: ammUsers[i] }) + + const qty = toBN(20000) + totalDeposits += Number(qty.toString()) + userBalance[i] += Number(qty.toString()) + await bamm.deposit(qty, { from: ammUsers[i] }) + await stabilityPool.provideToSP(qty, "0x0000000000000000000000000000000000000000", { from: nonAmmUsers[i] }) + } + + for(n = 0 ; n < 10 ; n++) { + for(let i = 0 ; i < ammUsers.length ; i++) { + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR * (i + n + 1), web3.currentProvider) + assert(almostTheSame((await lqtyToken.balanceOf(ammUsers[i])).toString(), (await lqtyToken.balanceOf(nonAmmUsers[i])).toString())) + assert.equal((await lusdToken.balanceOf(ammUsers[i])).toString(), (await lusdToken.balanceOf(nonAmmUsers[i])).toString()) + + const qty = (i+1) * 1000 + (n+1)*1000 // small number as 0 decimals + if((n*7 + i*3) % 2 === 0) { + const share = (await bamm.total()).mul(toBN(qty)).div(toBN(totalDeposits)) + console.log("withdraw", i, {qty}, {totalDeposits}, share.toString()) + await bamm.withdraw(share.toString(), { from: ammUsers[i] }) + await stabilityPool.withdrawFromSP(qty, { from: nonAmmUsers[i] }) + + totalDeposits -= qty + userBalance[i] -= qty + } + else { + console.log("deposit", i) + await bamm.deposit(qty, { from: ammUsers[i]} ) + await stabilityPool.provideToSP(qty, "0x0000000000000000000000000000000000000000", { from: nonAmmUsers[i] }) + + totalDeposits += qty + userBalance[i] += qty + } + + const totalSupply = await bamm.totalSupply() + const userSupply = await bamm.balanceOf(ammUsers[i]) + // userSup / totalSupply = userBalance / totalDeposits + assert.equal(userSupply.mul(toBN(totalDeposits)).toString(), toBN(userBalance[i]).mul(totalSupply).toString()) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR * (i + n + 1), web3.currentProvider) + + await bamm.withdraw(0, { from: ammUsers[i] }) + await stabilityPool.withdrawFromSP(0, { from: nonAmmUsers[i] }) + + await bamm.withdraw(0, { from: ammUsers[0] }) + await stabilityPool.withdrawFromSP(0, { from: nonAmmUsers[0] }) + + assert.equal((await lusdToken.balanceOf(ammUsers[i])).toString(), (await lusdToken.balanceOf(nonAmmUsers[i])).toString()) + assert(almostTheSame((await lqtyToken.balanceOf(ammUsers[i])).toString(), (await lqtyToken.balanceOf(nonAmmUsers[i])).toString())) + assert(almostTheSame((await lqtyToken.balanceOf(ammUsers[0])).toString(), (await lqtyToken.balanceOf(nonAmmUsers[0])).toString())) + } + } + + console.log("get all lqty") + for(let i = 0 ; i < ammUsers.length ; i++) { + await bamm.withdraw(0, { from: ammUsers[i] }) + await stabilityPool.withdrawFromSP(0, { from: nonAmmUsers[i] }) + } + + for(let i = 0 ; i < ammUsers.length ; i++) { + assert(almostTheSame((await lqtyToken.balanceOf(ammUsers[i])).toString(), (await lqtyToken.balanceOf(nonAmmUsers[i])).toString())) + } + }) + it("test complex LQTY allocation", async () => { await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) @@ -340,13 +422,26 @@ contract('StabilityPool', async accounts => { }) // tests: - // 1. complex lqty staking - // 2. complex share + // 1. complex lqty staking + share V + // 2. share test with ether // 3. basic share with liquidation // 4. price that exceeds max discount // 5. price that exceeds balance // 6. set params // 7. test with front end // 8. formula + // 9. lp token }) }) + + +function almostTheSame(n1, n2) { + n1 = Number(web3.utils.fromWei(n1)) + n2 = Number(web3.utils.fromWei(n2)) + //console.log(n1,n2) + + if(n1 * 1000 > n2 * 1001) return false + if(n2 * 1000 > n1 * 1001) return false + return true +} + From dfd272b010ae7705d4f25c3f664136cfd47391e0 Mon Sep 17 00:00:00 2001 From: yaron velner Date: Sun, 27 Jun 2021 00:09:47 +0300 Subject: [PATCH 04/90] fuzzy price formula test --- .../test/B.Protocol/PriceFormulaTest.js | 61 ++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/packages/contracts/test/B.Protocol/PriceFormulaTest.js b/packages/contracts/test/B.Protocol/PriceFormulaTest.js index 91ab1717f..9df5959ba 100644 --- a/packages/contracts/test/B.Protocol/PriceFormulaTest.js +++ b/packages/contracts/test/B.Protocol/PriceFormulaTest.js @@ -1,6 +1,6 @@ const Decimal = require("decimal.js"); -const { BNConverter } = require("../utils/BNConverter.js") -const testHelpers = require("../utils/testHelpers.js") +const { BNConverter } = require("./../../utils/BNConverter.js") +const testHelpers = require("./../../utils/testHelpers.js") const PriceFormula = artifacts.require("./PriceFormula.sol") const th = testHelpers.TestHelper @@ -27,5 +27,62 @@ contract('PriceFormula tests', async accounts => { const retAfterFee = ret.sub(ret.mul(toBN(4000000)).div(toBN(10**10))) assert.equal(retAfterFee.toString(10), '1231543859') }) + + it("fuzzy", async () => { + const A = 3 + const aStep = 7 + const xQty = "1234567891" + const xBalance = "321851652450" + const yBalance = "219413622039" + + const As = [] + const xQtys = [] + const xBalances = [] + const yBalances = [] + + const excpectedResult = [1188895769, 2411018031, 3638812385, 4868601476, 6099325960, 7330566424, 8562123398, 9793889769, 11025802785, 12257823179, 13489925074, 14722090684, 15954307349, 17186565794, 18418859045, 19651181742, 20883529689, 22115899540, 23348288590, 24580694617, 25813115778, 27045550524, 28277997542, 29510455706, 30742924044, 31975401710, 33207887963, 34440382148, 35672883685, 36905392053, 38137906789, 39370427472, 40602953723, 41835485196, 43068021577, 44300562576, 45533107928, 46765657391, 47998210737, 49230767758, 50463328261, 51695892065, 52928459002, 54161028915, 55393601657, 56626177090, 57858755085, 59091335520, 60323918281, 61556503261, 62789090357, 64021679473, 65254270518, 66486863407, 67719458057, 68952054392, 70184652337, 71417251823, 72649852784, 73882455156, 75115058879, 76347663896, 77580270153, 78812877597, 80045486178, 81278095850, 82510706566, 83743318283, 84975930960, 86208544556, 87441159035, 88673774360, 89906390495, 91139007408, 92371625066, 93604243438, 94836862496, 96069482210, 97302102554, 98534723502, 99767345029, 100999967109, 102232589722, 103465212843, 104697836453, 105930460530, 107163085054, 108395710007, 109628335370, 110860961126, 112093587257, 113326213748, 114558840582, 115791467745, 117024095222, 118256722999, 119489351063, 120721979399, 121954607997, 123187236843] + + assert(almost("123456", "123456")) + assert(almost("123455", "123456")) + assert(almost("123455", "123454")) + assert(!almost("123455", "123453")) + assert(!almost("123451", "123453")) + + for(let i = 0 ; i < 100 ; i++) { + const newA = A + aStep*(i+1) + const qty = web3.utils.toBN(xQty).mul(toBN(i+1)) + const xbalance = web3.utils.toBN(xBalance).add(qty.mul(toBN(3))) + const ybalance = web3.utils.toBN(yBalance).add(qty) + + console.log(newA.toString(), qty.toString(), xbalance.toString(), ybalance.toString()) + + console.log(i) + const ret = await priceFormula.getReturn(qty.toString(), xbalance.toString(), ybalance.toString(), newA); + console.log(ret.toString(), excpectedResult[i], Number(web3.utils.fromWei(ret.toString())) - Number(web3.utils.fromWei(excpectedResult[i].toString()))) + assert(almost(ret, excpectedResult[i])) + //assert.equal(ret.toString(), (excpectedResult[i] - 1).toString()) + + As.push(newA) + xQtys.push(qty.toString()) + xBalances.push(xbalance.toString()) + yBalances.push(ybalance.toString()) + } + + //console.log("A = [", As.toString(), "]") + //console.log("dx = [", xQtys.toString(), "]") + //console.log("x = [", xBalances.toString(), "]") + //console.log("y = [", yBalances.toString(), "]") + }) }) +function almost(n1, n2) { + const x = toBN(n1) + const y = toBN(n2) + + if(x.toString() === y.toString()) return true + if(x.add(toBN(1)).toString() === y.toString()) return true + if(y.add(toBN(1)).toString() === x.toString()) return true + + return false +} + From a596c94ba44ebe688ac76b831f50a1c5eb5fcfc8 Mon Sep 17 00:00:00 2001 From: yaron velner Date: Sun, 27 Jun 2021 09:57:07 +0300 Subject: [PATCH 05/90] share + eth test --- .../contracts/test/B.Protocol/BAMMTest.js | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/contracts/test/B.Protocol/BAMMTest.js b/packages/contracts/test/B.Protocol/BAMMTest.js index a2d4d4264..b86747d42 100644 --- a/packages/contracts/test/B.Protocol/BAMMTest.js +++ b/packages/contracts/test/B.Protocol/BAMMTest.js @@ -421,15 +421,59 @@ contract('BAMM', async accounts => { assert.equal(F_LQTYBalance_After.toString(), B_LQTYBalance_After.toString()) }) + it.only('test share with ether', async () => { + // --- SETUP --- + + // Whale opens Trove and deposits to SP + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + + const whaleLUSD = await lusdToken.balanceOf(whale) + await lusdToken.approve(bamm.address, whaleLUSD, { from: whale }) + await lusdToken.approve(bamm.address, toBN(dec(10000, 18)), { from: A }) + await bamm.deposit(toBN(dec(10000, 18)), { from: A } ) + + // 2 Troves opened, each withdraws minimum debt + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1, } }) + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2, } }) + + + // price drops: defaulter's Troves fall below MCR, whale doesn't + await priceFeed.setPrice(dec(105, 18)); + + // Troves are closed + await troveManager.liquidate(defaulter_1, { from: owner }) + await troveManager.liquidate(defaulter_2, { from: owner }) + + // 4k liquidations + assert.equal(toBN(dec(6000, 18)).toString(), (await stabilityPool.getCompoundedLUSDDeposit(bamm.address)).toString()) + const ethGains = web3.utils.toBN("39799999999999999975") + //console.log(ethGains.toString(), (await stabilityPool.getDepositorETHGain(bamm.address)).toString()) + + // send some ETH to simulate partial rebalance + await web3.eth.sendTransaction({from: whale, to: bamm.address, value: toBN(dec(1, 18))}) + assert.equal(toBN(await web3.eth.getBalance(bamm.address)).toString(), toBN(dec(1, 18)).toString()) + + const totalEth = ethGains.add(toBN(dec(1, 18))) + const totalUsd = toBN(dec(6000, 18)).add(totalEth.mul(toBN(105))) + + await lusdToken.approve(bamm.address, totalUsd, { from: B }) + await bamm.deposit(totalUsd, { from: B } ) + + assert.equal((await bamm.balanceOf(A)).toString(), (await bamm.balanceOf(B)).toString()) + + }) + // tests: // 1. complex lqty staking + share V - // 2. share test with ether + // 2. share test with ether V // 3. basic share with liquidation // 4. price that exceeds max discount // 5. price that exceeds balance // 6. set params // 7. test with front end - // 8. formula + // 8. formula V // 9. lp token }) }) From abe744da5325aded0c0a7ca72fdf98704d9f4c15 Mon Sep 17 00:00:00 2001 From: yaron velner Date: Sun, 27 Jun 2021 10:40:54 +0300 Subject: [PATCH 06/90] withdraw with eth --- .../contracts/test/B.Protocol/BAMMTest.js | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/packages/contracts/test/B.Protocol/BAMMTest.js b/packages/contracts/test/B.Protocol/BAMMTest.js index b86747d42..d9fcd3e8c 100644 --- a/packages/contracts/test/B.Protocol/BAMMTest.js +++ b/packages/contracts/test/B.Protocol/BAMMTest.js @@ -421,13 +421,13 @@ contract('BAMM', async accounts => { assert.equal(F_LQTYBalance_After.toString(), B_LQTYBalance_After.toString()) }) - it.only('test share with ether', async () => { + it('test share with ether', async () => { // --- SETUP --- // Whale opens Trove and deposits to SP - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) - await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) - await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: B } }) const whaleLUSD = await lusdToken.balanceOf(whale) await lusdToken.approve(bamm.address, whaleLUSD, { from: whale }) @@ -463,12 +463,23 @@ contract('BAMM', async accounts => { assert.equal((await bamm.balanceOf(A)).toString(), (await bamm.balanceOf(B)).toString()) - }) + const ethBalanceBefore = toBN(await web3.eth.getBalance(A)) + const LUSDBefore = await lusdToken.balanceOf(A) + await bamm.withdraw(await bamm.balanceOf(A), {from: A, gasPrice: 0}) + const ethBalanceAfter = toBN(await web3.eth.getBalance(A)) + const LUSDAfter = await lusdToken.balanceOf(A) + + const withdrawUsdValue = LUSDAfter.sub(LUSDBefore).add((ethBalanceAfter.sub(ethBalanceBefore)).mul(toBN(105))) + assert(in100WeiRadius(withdrawUsdValue.toString(), totalUsd.toString())) + + assert(in100WeiRadius("10283999999999999997375", "10283999999999999997322")) + assert(! in100WeiRadius("10283999999999999996375", "10283999999999999997322")) + }) // tests: // 1. complex lqty staking + share V // 2. share test with ether V - // 3. basic share with liquidation + // 3. basic share with liquidation (withdraw after liquidation) V // 4. price that exceeds max discount // 5. price that exceeds balance // 6. set params @@ -489,3 +500,12 @@ function almostTheSame(n1, n2) { return true } +function in100WeiRadius(n1, n2) { + const x = toBN(n1) + const y = toBN(n2) + + if(x.add(toBN(100)).lt(y)) return false + if(y.add(toBN(100)).lt(x)) return false + + return true +} \ No newline at end of file From a5dc53de6c719529c1fd1b4004dabd4251155b68 Mon Sep 17 00:00:00 2001 From: yaron velner Date: Sun, 27 Jun 2021 17:37:22 +0300 Subject: [PATCH 07/90] 4% max test --- .../contracts/test/B.Protocol/BAMMTest.js | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/packages/contracts/test/B.Protocol/BAMMTest.js b/packages/contracts/test/B.Protocol/BAMMTest.js index d9fcd3e8c..dfc6b20b8 100644 --- a/packages/contracts/test/B.Protocol/BAMMTest.js +++ b/packages/contracts/test/B.Protocol/BAMMTest.js @@ -29,6 +29,7 @@ contract('BAMM', async accounts => { u1, u2, u3, u4, u5, v1, v2, v3, v4, v5, frontEnd_1, frontEnd_2, frontEnd_3, + bammOwner ] = accounts; const [bountyAddress, lpRewardsAddress, multisig] = accounts.slice(997, 1000) @@ -94,7 +95,7 @@ contract('BAMM', async accounts => { // deploy BAMM chainlink = await ChainlinkTestnet.new(priceFeed.address) - bamm = await BAMM.new(chainlink.address, stabilityPool.address, lusdToken.address, lqtyToken.address, 400, feePool) + bamm = await BAMM.new(chainlink.address, stabilityPool.address, lusdToken.address, lqtyToken.address, 400, feePool, {from: bammOwner}) }) // --- provideToSP() --- @@ -476,12 +477,52 @@ contract('BAMM', async accounts => { assert(! in100WeiRadius("10283999999999999996375", "10283999999999999997322")) }) + it('price exceed max dicount', async () => { + // --- SETUP --- + + // Whale opens Trove and deposits to SP + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: B } }) + + const whaleLUSD = await lusdToken.balanceOf(whale) + await lusdToken.approve(bamm.address, whaleLUSD, { from: whale }) + await lusdToken.approve(bamm.address, toBN(dec(10000, 18)), { from: A }) + await bamm.deposit(toBN(dec(10000, 18)), { from: A } ) + + // 2 Troves opened, each withdraws minimum debt + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1, } }) + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2, } }) + + + // price drops: defaulter's Troves fall below MCR, whale doesn't + await priceFeed.setPrice(dec(105, 18)); + + // Troves are closed + await troveManager.liquidate(defaulter_1, { from: owner }) + await troveManager.liquidate(defaulter_2, { from: owner }) + + // 4k liquidations + assert.equal(toBN(dec(6000, 18)).toString(), (await stabilityPool.getCompoundedLUSDDeposit(bamm.address)).toString()) + const ethGains = web3.utils.toBN("39799999999999999975") + + // without fee + await bamm.setParams(20, 0, {from: bammOwner}) + const price = await bamm.getSwapEthAmount(dec(105, 18)) + assert.equal(price.ethAmount.toString(), dec(104, 18-2).toString()) + + // with fee + await bamm.setParams(20, 100, {from: bammOwner}) + const priceWithFee = await bamm.getSwapEthAmount(dec(105, 18)) + assert.equal(priceWithFee.ethAmount.toString(), dec(10296, 18-4).toString()) + }) // tests: // 1. complex lqty staking + share V // 2. share test with ether V // 3. basic share with liquidation (withdraw after liquidation) V - // 4. price that exceeds max discount + // 4. price that exceeds max discount V // 5. price that exceeds balance + // 5.5 test fees and return // 6. set params // 7. test with front end // 8. formula V From 9d1059540cb54673a4a9ab1fb093e959bb4b5cc0 Mon Sep 17 00:00:00 2001 From: yaron velner Date: Sun, 27 Jun 2021 18:36:24 +0300 Subject: [PATCH 08/90] price > eth balance --- packages/contracts/test/B.Protocol/BAMMTest.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/contracts/test/B.Protocol/BAMMTest.js b/packages/contracts/test/B.Protocol/BAMMTest.js index dfc6b20b8..50f5d353f 100644 --- a/packages/contracts/test/B.Protocol/BAMMTest.js +++ b/packages/contracts/test/B.Protocol/BAMMTest.js @@ -477,7 +477,7 @@ contract('BAMM', async accounts => { assert(! in100WeiRadius("10283999999999999996375", "10283999999999999997322")) }) - it('price exceed max dicount', async () => { + it('price exceed max dicount and/or eth balance', async () => { // --- SETUP --- // Whale opens Trove and deposits to SP @@ -515,13 +515,24 @@ contract('BAMM', async accounts => { await bamm.setParams(20, 100, {from: bammOwner}) const priceWithFee = await bamm.getSwapEthAmount(dec(105, 18)) assert.equal(priceWithFee.ethAmount.toString(), dec(10296, 18-4).toString()) - }) + + // without fee + await bamm.setParams(20, 0, {from: bammOwner}) + const priceDepleted = await bamm.getSwapEthAmount(dec(1050000000000000, 18)) + assert.equal(priceDepleted.ethAmount.toString(), ethGains.toString()) + + // with fee + await bamm.setParams(20, 100, {from: bammOwner}) + const priceDepletedWithFee = await bamm.getSwapEthAmount(dec(1050000000000000, 18)) + assert.equal(priceDepletedWithFee.ethAmount.toString(), ethGains.mul(toBN(99)).div(toBN(100))) + }) + // tests: // 1. complex lqty staking + share V // 2. share test with ether V // 3. basic share with liquidation (withdraw after liquidation) V // 4. price that exceeds max discount V - // 5. price that exceeds balance + // 5. price that exceeds balance V // 5.5 test fees and return // 6. set params // 7. test with front end From 38bb5b926960cfa510babc114edb6891de7c7d18 Mon Sep 17 00:00:00 2001 From: yaron velner Date: Tue, 29 Jun 2021 11:32:35 +0300 Subject: [PATCH 09/90] more tests --- .../contracts/B.Protocol/ChainlinkTestnet.sol | 8 +- .../contracts/test/B.Protocol/BAMMTest.js | 104 +++++++++++++++++- 2 files changed, 110 insertions(+), 2 deletions(-) diff --git a/packages/contracts/contracts/B.Protocol/ChainlinkTestnet.sol b/packages/contracts/contracts/B.Protocol/ChainlinkTestnet.sol index d0edb219c..74a54d6a7 100644 --- a/packages/contracts/contracts/B.Protocol/ChainlinkTestnet.sol +++ b/packages/contracts/contracts/B.Protocol/ChainlinkTestnet.sol @@ -11,6 +11,7 @@ import "./../TestContracts/PriceFeedTestnet.sol"; contract ChainlinkTestnet { PriceFeedTestnet feed; + uint time = 0; constructor(PriceFeedTestnet _feed) public { feed = _feed; @@ -20,6 +21,10 @@ contract ChainlinkTestnet { return 18; } + function setTimestamp(uint _time) external { + time = _time; + } + function latestRoundData() external view returns ( uint80 roundId, @@ -30,6 +35,7 @@ contract ChainlinkTestnet { ) { answer = int(feed.getPrice()); - timestamp = now; + if(time == 0 ) timestamp = now; + else timestamp = time; } } diff --git a/packages/contracts/test/B.Protocol/BAMMTest.js b/packages/contracts/test/B.Protocol/BAMMTest.js index 50f5d353f..34311f5a7 100644 --- a/packages/contracts/test/B.Protocol/BAMMTest.js +++ b/packages/contracts/test/B.Protocol/BAMMTest.js @@ -526,6 +526,106 @@ contract('BAMM', async accounts => { const priceDepletedWithFee = await bamm.getSwapEthAmount(dec(1050000000000000, 18)) assert.equal(priceDepletedWithFee.ethAmount.toString(), ethGains.mul(toBN(99)).div(toBN(100))) }) + + it('test getSwapEthAmount', async () => { + // --- SETUP --- + + // Whale opens Trove and deposits to SP + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: B } }) + + const whaleLUSD = await lusdToken.balanceOf(whale) + await lusdToken.approve(bamm.address, whaleLUSD, { from: whale }) + await lusdToken.approve(bamm.address, toBN(dec(10000, 18)), { from: A }) + await bamm.deposit(toBN(dec(10000, 18)), { from: A } ) + + // 2 Troves opened, each withdraws minimum debt + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1, } }) + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2, } }) + + + // price drops: defaulter's Troves fall below MCR, whale doesn't + await priceFeed.setPrice(dec(105, 18)); + + // Troves are closed + await troveManager.liquidate(defaulter_1, { from: owner }) + await troveManager.liquidate(defaulter_2, { from: owner }) + + // 4k liquidations + assert.equal(toBN(dec(6000, 18)).toString(), (await stabilityPool.getCompoundedLUSDDeposit(bamm.address)).toString()) + const ethGains = web3.utils.toBN("39799999999999999975") + + const lusdQty = dec(105, 18) + const expectedReturn = await bamm.getReturn(lusdQty, dec(6000, 18), toBN(dec(6000, 18)).add(ethGains.mul(toBN(2 * 105))), 200) + + // without fee + await bamm.setParams(200, 0, {from: bammOwner}) + const priceWithoutFee = await bamm.getSwapEthAmount(lusdQty) + assert.equal(priceWithoutFee.ethAmount.toString(), expectedReturn.mul(toBN(100)).div(toBN(100 * 105)).toString()) + + // with fee + await bamm.setParams(200, 100, {from: bammOwner}) + const priceWithFee = await bamm.getSwapEthAmount(lusdQty) + assert.equal(priceWithFee.ethAmount.toString(), expectedReturn.mul(toBN(99)).div(toBN(100 * 105)).toString()) + }) + + it('test fetch price', async () => { + await priceFeed.setPrice(dec(666, 18)); + assert.equal(await bamm.fetchPrice(), dec(666, 18)) + + await chainlink.setTimestamp(888) + assert.equal((await bamm.fetchPrice()).toString(), "0") + }) + + it('test swap', async () => { + // --- SETUP --- + + // Whale opens Trove and deposits to SP + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: B } }) + + const whaleLUSD = await lusdToken.balanceOf(whale) + await lusdToken.approve(bamm.address, whaleLUSD, { from: whale }) + await lusdToken.approve(bamm.address, toBN(dec(10000, 18)), { from: A }) + await bamm.deposit(toBN(dec(10000, 18)), { from: A } ) + + // 2 Troves opened, each withdraws minimum debt + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1, } }) + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2, } }) + + + // price drops: defaulter's Troves fall below MCR, whale doesn't + await priceFeed.setPrice(dec(105, 18)); + + // Troves are closed + await troveManager.liquidate(defaulter_1, { from: owner }) + await troveManager.liquidate(defaulter_2, { from: owner }) + + // 4k liquidations + assert.equal(toBN(dec(6000, 18)).toString(), (await stabilityPool.getCompoundedLUSDDeposit(bamm.address)).toString()) + const ethGains = web3.utils.toBN("39799999999999999975") + + // with fee + await bamm.setParams(20, 100, {from: bammOwner}) + const priceWithFee = await bamm.getSwapEthAmount(dec(105, 18)) + assert.equal(priceWithFee.ethAmount.toString(), dec(10296, 18-4).toString()) + assert.equal(priceWithFee.feeEthAmount.toString(), dec(10400 - 10296, 18-4).toString()) + + await lusdToken.approve(bamm.address, dec(1,18), {from: whale}) + const dest = "0xdEADBEEF00AA81bBCF694bC5c05A397F5E5658D5" + await bamm.swap(dec(1,18), dest, {from: whale}) + + // check lusd balance + assert.equal(toBN(dec(6001, 18)).toString(), (await stabilityPool.getCompoundedLUSDDeposit(bamm.address)).toString()) + + // check eth balance + assert(await web3.eth.getBalance(dest), priceWithFee.ethAmount) + + // check fees + assert(await web3.eth.getBalance(feePool), priceWithFee.feeEthAmount) + }) // tests: // 1. complex lqty staking + share V @@ -533,7 +633,9 @@ contract('BAMM', async accounts => { // 3. basic share with liquidation (withdraw after liquidation) V // 4. price that exceeds max discount V // 5. price that exceeds balance V - // 5.5 test fees and return + // 5.5 test fees and return V + // 5.6 test swap v + // 6.1 test fetch price V // 6. set params // 7. test with front end // 8. formula V From 8d072fa87f5ecba8588687579b092f63b38f2c67 Mon Sep 17 00:00:00 2001 From: yaron velner Date: Tue, 29 Jun 2021 12:02:26 +0300 Subject: [PATCH 10/90] set params test --- .../contracts/test/B.Protocol/BAMMTest.js | 72 ++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/packages/contracts/test/B.Protocol/BAMMTest.js b/packages/contracts/test/B.Protocol/BAMMTest.js index 34311f5a7..23de58176 100644 --- a/packages/contracts/test/B.Protocol/BAMMTest.js +++ b/packages/contracts/test/B.Protocol/BAMMTest.js @@ -55,7 +55,7 @@ contract('BAMM', async accounts => { const getOpenTroveLUSDAmount = async (totalDebt) => th.getOpenTroveLUSDAmount(contracts, totalDebt) const openTrove = async (params) => th.openTrove(contracts, params) - const assertRevert = th.assertRevert + //const assertRevert = th.assertRevert describe("BAMM", async () => { @@ -626,7 +626,60 @@ contract('BAMM', async accounts => { // check fees assert(await web3.eth.getBalance(feePool), priceWithFee.feeEthAmount) }) + + it('test set params happy path', async () => { + // --- SETUP --- + + // Whale opens Trove and deposits to SP + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: B } }) + + const whaleLUSD = await lusdToken.balanceOf(whale) + await lusdToken.approve(bamm.address, whaleLUSD, { from: whale }) + await lusdToken.approve(bamm.address, toBN(dec(10000, 18)), { from: A }) + await bamm.deposit(toBN(dec(10000, 18)), { from: A } ) + + // 2 Troves opened, each withdraws minimum debt + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1, } }) + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2, } }) + + + // price drops: defaulter's Troves fall below MCR, whale doesn't + await priceFeed.setPrice(dec(105, 18)); + + // Troves are closed + await troveManager.liquidate(defaulter_1, { from: owner }) + await troveManager.liquidate(defaulter_2, { from: owner }) + + // 4k liquidations + assert.equal(toBN(dec(6000, 18)).toString(), (await stabilityPool.getCompoundedLUSDDeposit(bamm.address)).toString()) + const ethGains = web3.utils.toBN("39799999999999999975") + + const lusdQty = dec(105, 18) + const expectedReturn200 = await bamm.getReturn(lusdQty, dec(6000, 18), toBN(dec(6000, 18)).add(ethGains.mul(toBN(2 * 105))), 200) + const expectedReturn190 = await bamm.getReturn(lusdQty, dec(6000, 18), toBN(dec(6000, 18)).add(ethGains.mul(toBN(2 * 105))), 190) + + assert(expectedReturn200.toString() !== expectedReturn190.toString()) + + // without fee + await bamm.setParams(200, 0, {from: bammOwner}) + const priceWithoutFee = await bamm.getSwapEthAmount(lusdQty) + assert.equal(priceWithoutFee.ethAmount.toString(), expectedReturn200.mul(toBN(100)).div(toBN(100 * 105)).toString()) + + // with fee + await bamm.setParams(190, 100, {from: bammOwner}) + const priceWithFee = await bamm.getSwapEthAmount(lusdQty) + assert.equal(priceWithFee.ethAmount.toString(), expectedReturn190.mul(toBN(99)).div(toBN(100 * 105)).toString()) + }) + it('test set params sad path', async () => { + await assertRevert(bamm.setParams(210, 100, {from: bammOwner}), 'setParams: A too big') + await assertRevert(bamm.setParams(10, 100, {from: bammOwner}), 'setParams: A too small') + await assertRevert(bamm.setParams(10, 101, {from: bammOwner}), 'setParams: fee is too big') + await assertRevert(bamm.setParams(20, 100, {from: B}), 'Ownable: caller is not the owner') + }) + // tests: // 1. complex lqty staking + share V // 2. share test with ether V @@ -636,7 +689,7 @@ contract('BAMM', async accounts => { // 5.5 test fees and return V // 5.6 test swap v // 6.1 test fetch price V - // 6. set params + // 6. set params V // 7. test with front end // 8. formula V // 9. lp token @@ -662,4 +715,19 @@ function in100WeiRadius(n1, n2) { if(y.add(toBN(100)).lt(x)) return false return true +} + +async function assertRevert(txPromise, message = undefined) { + try { + const tx = await txPromise + // console.log("tx succeeded") + assert.isFalse(tx.receipt.status) // when this assert fails, the expected revert didn't occur, i.e. the tx succeeded + } catch (err) { + // console.log("tx failed") + assert.include(err.message, "revert") + + if (message) { + assert.include(err.message, message) + } + } } \ No newline at end of file From 072acde618ad85a0c94035df7ca9bb48e4c7b953 Mon Sep 17 00:00:00 2001 From: yaron velner Date: Tue, 29 Jun 2021 12:21:58 +0300 Subject: [PATCH 11/90] front end support --- .../contracts/contracts/B.Protocol/BAMM.sol | 18 +++++++++++---- .../contracts/test/B.Protocol/BAMMTest.js | 22 ++++++++++++------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/packages/contracts/contracts/B.Protocol/BAMM.sol b/packages/contracts/contracts/B.Protocol/BAMM.sol index 26c77a74d..ad570d3d1 100644 --- a/packages/contracts/contracts/B.Protocol/BAMM.sol +++ b/packages/contracts/contracts/B.Protocol/BAMM.sol @@ -27,18 +27,28 @@ contract BAMM is LPToken, PriceFormula, Ownable { uint public immutable maxDiscount; // max discount in bips - address public constant frontEndTag = address(0); + address public immutable frontEndTag; uint constant PRECISION = 1e18; - constructor(address _priceAggregator, address payable _SP, address _LUSD, address _LQTY, uint _maxDiscount, address payable _feePool) public - LPToken(_LQTY) { + constructor( + address _priceAggregator, + address payable _SP, + address _LUSD, + address _LQTY, + uint _maxDiscount, + address payable _feePool, + address _fronEndTag) + public + LPToken(_LQTY) + { priceAggregator = AggregatorV3Interface(_priceAggregator); SP = StabilityPool(_SP); LUSD = IERC20(_LUSD); feePool = _feePool; maxDiscount = _maxDiscount; + frontEndTag = _fronEndTag; } function setParams(uint _A, uint _fee) external onlyOwner { @@ -165,7 +175,7 @@ contract BAMM is LPToken, PriceFormula, Ownable { function swap(uint lusdAmount, address payable dest) public payable returns(uint) { (uint ethAmount, uint feeAmount) = getSwapEthAmount(lusdAmount); LUSD.transferFrom(msg.sender, address(this), lusdAmount); - SP.provideToSP(lusdAmount, frontEndTag); // TODO - real front end + SP.provideToSP(lusdAmount, frontEndTag); if(feeAmount > 0) feePool.transfer(feeAmount); (bool success, ) = dest.call{ value: ethAmount }(""); // re-entry is fine here diff --git a/packages/contracts/test/B.Protocol/BAMMTest.js b/packages/contracts/test/B.Protocol/BAMMTest.js index 23de58176..eb492ee68 100644 --- a/packages/contracts/test/B.Protocol/BAMMTest.js +++ b/packages/contracts/test/B.Protocol/BAMMTest.js @@ -91,11 +91,15 @@ contract('BAMM', async accounts => { await deploymentHelper.connectLQTYContractsToCore(LQTYContracts, contracts) // Register 3 front ends - await th.registerFrontEnds(frontEnds, stabilityPool) + //await th.registerFrontEnds(frontEnds, stabilityPool) // deploy BAMM chainlink = await ChainlinkTestnet.new(priceFeed.address) - bamm = await BAMM.new(chainlink.address, stabilityPool.address, lusdToken.address, lqtyToken.address, 400, feePool, {from: bammOwner}) + + const kickbackRate_F1 = toBN(dec(5, 17)) // F1 kicks 50% back to depositor + await stabilityPool.registerFrontEnd(kickbackRate_F1, { from: frontEnd_1 }) + + bamm = await BAMM.new(chainlink.address, stabilityPool.address, lusdToken.address, lqtyToken.address, 400, feePool, frontEnd_1, {from: bammOwner}) }) // --- provideToSP() --- @@ -233,7 +237,7 @@ contract('BAMM', async accounts => { await lusdToken.approve(bamm.address, dec(2000, 18), { from: E }) await bamm.deposit(dec(1000, 18), { from: D }) await bamm.deposit(dec(2000, 18), { from: E }) - await stabilityPool.provideToSP(dec(3000, 18), "0x0000000000000000000000000000000000000000", { from: F }) + await stabilityPool.provideToSP(dec(3000, 18), frontEnd_1, { from: F }) // Get F1, F2, F3 LQTY balances before, and confirm they're zero const D_LQTYBalance_Before = await lqtyToken.balanceOf(D) @@ -255,6 +259,8 @@ contract('BAMM', async accounts => { const E_LQTYBalance_After = await lqtyToken.balanceOf(E) const F_LQTYBalance_After = await lqtyToken.balanceOf(F) + assert((await lqtyToken.balanceOf(frontEnd_1)).gt(toBN(0))) + assert.equal(D_LQTYBalance_After.add(E_LQTYBalance_After).toString(), F_LQTYBalance_After.toString()) }) @@ -280,7 +286,7 @@ contract('BAMM', async accounts => { totalDeposits += Number(qty.toString()) userBalance[i] += Number(qty.toString()) await bamm.deposit(qty, { from: ammUsers[i] }) - await stabilityPool.provideToSP(qty, "0x0000000000000000000000000000000000000000", { from: nonAmmUsers[i] }) + await stabilityPool.provideToSP(qty, frontEnd_1, { from: nonAmmUsers[i] }) } for(n = 0 ; n < 10 ; n++) { @@ -302,7 +308,7 @@ contract('BAMM', async accounts => { else { console.log("deposit", i) await bamm.deposit(qty, { from: ammUsers[i]} ) - await stabilityPool.provideToSP(qty, "0x0000000000000000000000000000000000000000", { from: nonAmmUsers[i] }) + await stabilityPool.provideToSP(qty, frontEnd_1, { from: nonAmmUsers[i] }) totalDeposits += qty userBalance[i] += qty @@ -375,12 +381,12 @@ contract('BAMM', async accounts => { console.log("stake D:", (await bamm.stake(D)).toString()) console.log("stake E:", (await bamm.stake(E)).toString()) - await stabilityPool.provideToSP(dec(1000, 18), "0x0000000000000000000000000000000000000000", { from: A }) + await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: A }) await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) await bamm.deposit(dec(3000, 18), { from: F }) - await stabilityPool.provideToSP(dec(3000, 18), "0x0000000000000000000000000000000000000000", { from: B }) + await stabilityPool.provideToSP(dec(3000, 18), frontEnd_1, { from: B }) await stabilityPool.withdrawFromSP(0, { from: A }) console.log("lqty A", (await lqtyToken.balanceOf(A)).toString()) @@ -690,7 +696,7 @@ contract('BAMM', async accounts => { // 5.6 test swap v // 6.1 test fetch price V // 6. set params V - // 7. test with front end + // 7. test with front end v // 8. formula V // 9. lp token }) From 509b28c91a7d93e7a5e2c878819bf99147a7e3c7 Mon Sep 17 00:00:00 2001 From: yaron velner Date: Tue, 29 Jun 2021 14:48:32 +0300 Subject: [PATCH 12/90] transfer --- .../contracts/contracts/B.Protocol/BAMM.sol | 4 +- .../contracts/B.Protocol/LPToken.sol | 18 +++++-- .../contracts/test/B.Protocol/BAMMTest.js | 51 +++++++++++++++++++ 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/packages/contracts/contracts/B.Protocol/BAMM.sol b/packages/contracts/contracts/B.Protocol/BAMM.sol index ad570d3d1..ba427c65e 100644 --- a/packages/contracts/contracts/B.Protocol/BAMM.sol +++ b/packages/contracts/contracts/B.Protocol/BAMM.sol @@ -15,7 +15,6 @@ contract BAMM is LPToken, PriceFormula, Ownable { using SafeMath for uint256; AggregatorV3Interface public immutable priceAggregator; - StabilityPool public immutable SP; IERC20 public immutable LUSD; address payable public immutable feePool; @@ -40,10 +39,9 @@ contract BAMM is LPToken, PriceFormula, Ownable { address payable _feePool, address _fronEndTag) public - LPToken(_LQTY) + LPToken(_LQTY, _SP) { priceAggregator = AggregatorV3Interface(_priceAggregator); - SP = StabilityPool(_SP); LUSD = IERC20(_LUSD); feePool = _feePool; diff --git a/packages/contracts/contracts/B.Protocol/LPToken.sol b/packages/contracts/contracts/B.Protocol/LPToken.sol index 628937bc5..7bcee5fbc 100644 --- a/packages/contracts/contracts/B.Protocol/LPToken.sol +++ b/packages/contracts/contracts/B.Protocol/LPToken.sol @@ -1,18 +1,22 @@ pragma solidity 0.6.11; import "./crop.sol"; +import "./../StabilityPool.sol"; contract LPToken is CropJoin { string constant public name = "B.AMM LUSD-ETH"; string constant public symbol = "LUSDETH"; uint constant public decimals = 18; mapping(address => mapping(address => uint)) allowance; + StabilityPool immutable public SP; event Transfer(address indexed _from, address indexed _to, uint256 _value); event Approval(address indexed _owner, address indexed _spender, uint256 _value); - constructor(address lqty) public - CropJoin(address(new Dummy()), "B.AMM", address(new DummyGem()), lqty) {} + constructor(address _lqty, address payable _SP) public + CropJoin(address(new Dummy()), "B.AMM", address(new DummyGem()), _lqty) { + SP = StabilityPool(_SP); + } function mint(address to, uint value) internal { join(to, value); @@ -33,20 +37,26 @@ contract LPToken is CropJoin { } function transfer(address to, uint256 value) public returns (bool success) { + // get all lqty to the contract + SP.withdrawFromSP(0); + burn(msg.sender, value); mint(to, value); - emit Transfer(msg.sender, to, value); + // event is emitted in burn and mint success = true; } function transferFrom(address from, address to, uint256 value) public returns (bool success) { + // get all lqty to the contract + SP.withdrawFromSP(0); + allowance[msg.sender][from] = sub(allowance[msg.sender][from], value); burn(from, value); mint(to, value); - emit Transfer(from, to, value); + // event is emitted in burn and mint success = true; } diff --git a/packages/contracts/test/B.Protocol/BAMMTest.js b/packages/contracts/test/B.Protocol/BAMMTest.js index eb492ee68..9b78dda9f 100644 --- a/packages/contracts/test/B.Protocol/BAMMTest.js +++ b/packages/contracts/test/B.Protocol/BAMMTest.js @@ -686,6 +686,56 @@ contract('BAMM', async accounts => { await assertRevert(bamm.setParams(20, 100, {from: B}), 'Ownable: caller is not the owner') }) + it('transfer test', async () => { + // --- SETUP --- + + // Whale opens Trove and deposits to SP + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: C } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: D } }) + + const whaleLUSD = await lusdToken.balanceOf(whale) + await lusdToken.approve(bamm.address, whaleLUSD, { from: whale }) + await lusdToken.approve(bamm.address, toBN(dec(10000, 18)), { from: A }) + await bamm.deposit(toBN(dec(10000, 18)), { from: A } ) + await stabilityPool.provideToSP(toBN(dec(10000, 18)), frontEnd_1, {from: C}) + + assert.equal(await bamm.balanceOf(A), dec(1, 18)) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + await stabilityPool.provideToSP(toBN(dec(5000, 18)), frontEnd_1, {from: D}) + + await bamm.transfer(B, dec(5, 17), {from: A}) + assert.equal(await bamm.balanceOf(A), dec(5, 17)) + assert.equal(await bamm.balanceOf(B), dec(5, 17)) + + await stabilityPool.withdrawFromSP(toBN(dec(5000, 18)), { from: C }) + assert.equal(await lqtyToken.balanceOf(B), "0") + await bamm.withdraw(0, {from: A}) + assert.equal((await lqtyToken.balanceOf(A)).toString(), (await lqtyToken.balanceOf(C)).toString()) + + // reset A's usd balance + await lusdToken.transfer(C, await lusdToken.balanceOf(A), {from: A}) + assert.equal(await lusdToken.balanceOf(A), "0") + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + await bamm.withdraw(toBN(dec(5, 17)), {from: A}) // check balance + await bamm.withdraw(toBN(dec(5, 17)), {from: B}) // check balance + await stabilityPool.withdrawFromSP(toBN(dec(5000, 18)), { from: C }) + await stabilityPool.withdrawFromSP(toBN(dec(5000, 18)), { from: D }) + + assert.equal((await lqtyToken.balanceOf(B)).toString(), (await lqtyToken.balanceOf(D)).toString()) + assert.equal((await lqtyToken.balanceOf(A)).toString(), (await lqtyToken.balanceOf(C)).toString()) + + assert.equal((await lusdToken.balanceOf(B)).toString(), dec(5000, 18)) + assert.equal((await lusdToken.balanceOf(A)).toString(), dec(5000, 18)) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + }) + // tests: // 1. complex lqty staking + share V // 2. share test with ether V @@ -699,6 +749,7 @@ contract('BAMM', async accounts => { // 7. test with front end v // 8. formula V // 9. lp token + // 10. cleanups }) }) From 91070b5d021358ce6e222ede265e7b65806c352c Mon Sep 17 00:00:00 2001 From: yaron velner Date: Wed, 30 Jun 2021 13:13:13 +0300 Subject: [PATCH 13/90] pickle support --- .../contracts/B.Protocol/LPToken.sol | 4 +- .../contracts/B.Protocol/MockePickle.sol | 93 ++ .../contracts/contracts/B.Protocol/PBAMM.sol | 47 + .../contracts/test/B.Protocol/BAMMTest.js | 3 +- .../contracts/test/B.Protocol/PickleTest.js | 802 ++++++++++++++++++ 5 files changed, 946 insertions(+), 3 deletions(-) create mode 100644 packages/contracts/contracts/B.Protocol/MockePickle.sol create mode 100644 packages/contracts/contracts/B.Protocol/PBAMM.sol create mode 100644 packages/contracts/test/B.Protocol/PickleTest.js diff --git a/packages/contracts/contracts/B.Protocol/LPToken.sol b/packages/contracts/contracts/B.Protocol/LPToken.sol index 7bcee5fbc..bde09ad28 100644 --- a/packages/contracts/contracts/B.Protocol/LPToken.sol +++ b/packages/contracts/contracts/B.Protocol/LPToken.sol @@ -18,12 +18,12 @@ contract LPToken is CropJoin { SP = StabilityPool(_SP); } - function mint(address to, uint value) internal { + function mint(address to, uint value) virtual internal { join(to, value); emit Transfer(address(0), to, value); } - function burn(address owner, uint value) internal { + function burn(address owner, uint value) virtual internal { exit(owner, value); emit Transfer(owner, address(0), value); } diff --git a/packages/contracts/contracts/B.Protocol/MockePickle.sol b/packages/contracts/contracts/B.Protocol/MockePickle.sol new file mode 100644 index 000000000..dc32e7210 --- /dev/null +++ b/packages/contracts/contracts/B.Protocol/MockePickle.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.11; + +contract EIP20 { + + event Transfer(address indexed _from, address indexed _to, uint256 _value); + event Approval(address indexed _owner, address indexed _spender, uint256 _value); + + uint256 public totalSupply; + uint256 constant private MAX_UINT256 = 2**256 - 1; + mapping (address => uint256) public balances; + mapping (address => mapping (address => uint256)) public allowed; + /* + NOTE: + The following variables are OPTIONAL vanities. One does not have to include them. + They allow one to customise the token contract & in no way influences the core functionality. + Some wallets/interfaces might not even bother to look at this information. + */ + string public name; //fancy name: eg Simon Bucks + uint8 public decimals; //How many decimals to show. + string public symbol; //An identifier: eg SBX + + constructor ( + uint256 _initialAmount, + string memory _tokenName, + uint8 _decimalUnits, + string memory _tokenSymbol + ) public { + balances[msg.sender] = _initialAmount; // Give the creator all initial tokens + totalSupply = _initialAmount; // Update total supply + name = _tokenName; // Set the name for display purposes + decimals = _decimalUnits; // Amount of decimals for display purposes + symbol = _tokenSymbol; // Set the symbol for display purposes + } + + function transfer(address _to, uint256 _value) public returns (bool success) { + require(balances[msg.sender] >= _value); + balances[msg.sender] -= _value; + balances[_to] += _value; + emit Transfer(msg.sender, _to, _value); //solhint-disable-line indent, no-unused-vars + return true; + } + + function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) { + uint256 allowance = allowed[_from][msg.sender]; + require(balances[_from] >= _value && allowance >= _value); + balances[_to] += _value; + balances[_from] -= _value; + if (allowance < MAX_UINT256) { + allowed[_from][msg.sender] -= _value; + } + emit Transfer(_from, _to, _value); //solhint-disable-line indent, no-unused-vars + return true; + } + + function balanceOf(address _owner) public view returns (uint256 balance) { + return balances[_owner]; + } + + function approve(address _spender, uint256 _value) public returns (bool success) { + allowed[msg.sender][_spender] = _value; + emit Approval(msg.sender, _spender, _value); //solhint-disable-line indent, no-unused-vars + return true; + } + + function allowance(address _owner, address _spender) public view returns (uint256 remaining) { + return allowed[_owner][_spender]; + } + + // only for mock + function mint(address _to, uint _qty) public { + balances[_to] += _qty; + } +} + +contract PickleJar { + EIP20 public token; + EIP20 public pToken; + + constructor(EIP20 _token, EIP20 _ptoken) public { + token = _token; + pToken = _ptoken; + } + + function depositAll() external { + uint userBalance = token.balanceOf(msg.sender); + require(token.transferFrom(msg.sender, address(this), userBalance), "depositAll: transferFrom failed"); + pToken.mint(msg.sender, userBalance / 2); // 1 share = 2 token + } +} + + diff --git a/packages/contracts/contracts/B.Protocol/PBAMM.sol b/packages/contracts/contracts/B.Protocol/PBAMM.sol new file mode 100644 index 000000000..4a2ba74b5 --- /dev/null +++ b/packages/contracts/contracts/B.Protocol/PBAMM.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.11; + +import "./BAMM.sol"; + +interface PickleJarLike { + function depositAll() external; +} + +contract PBAMM is BAMM { + PickleJarLike immutable pickleJar; + + constructor( + address _priceAggregator, + address payable _SP, + address _LUSD, + address _LQTY, + uint _maxDiscount, + address payable _feePool, + address _frontEndTag, + address _pLQTY, + address _pickleJar) + public + BAMM(_priceAggregator, _SP, _LUSD, _pLQTY, _maxDiscount, _feePool, _frontEndTag) + { + pickleJar = PickleJarLike(_pickleJar); + + require(IERC20(_LQTY).approve(_pickleJar, type(uint).max), "constructor: approve failed"); + } + + function mint(address to, uint value) override internal { + pickleJar.depositAll(); + super.mint(to, value); + } + + function burn(address owner, uint value) override internal { + pickleJar.depositAll(); + super.burn(owner, value); + } + + // callable by anyone + function depositLqty() external { + SP.withdrawFromSP(0); + pickleJar.depositAll(); + } +} diff --git a/packages/contracts/test/B.Protocol/BAMMTest.js b/packages/contracts/test/B.Protocol/BAMMTest.js index 9b78dda9f..5e440f7d0 100644 --- a/packages/contracts/test/B.Protocol/BAMMTest.js +++ b/packages/contracts/test/B.Protocol/BAMMTest.js @@ -749,7 +749,8 @@ contract('BAMM', async accounts => { // 7. test with front end v // 8. formula V // 9. lp token - // 10. cleanups + // 11. pickle + // 10. cleanups - compilation warnings. cropjoin - revoke changes and maybe make internal. }) }) diff --git a/packages/contracts/test/B.Protocol/PickleTest.js b/packages/contracts/test/B.Protocol/PickleTest.js new file mode 100644 index 000000000..3c270a39b --- /dev/null +++ b/packages/contracts/test/B.Protocol/PickleTest.js @@ -0,0 +1,802 @@ +const deploymentHelper = require("./../../utils/deploymentHelpers.js") +const testHelpers = require("./../../utils/testHelpers.js") +const th = testHelpers.TestHelper +const dec = th.dec +const toBN = th.toBN +const mv = testHelpers.MoneyValues +const timeValues = testHelpers.TimeValues + +const TroveManagerTester = artifacts.require("TroveManagerTester") +const LUSDToken = artifacts.require("LUSDToken") +const NonPayable = artifacts.require('NonPayable.sol') +const BAMM = artifacts.require("PBAMM.sol") +const EIP20 = artifacts.require("EIP20.sol") +const Pickle = artifacts.require("PickleJar.sol") +const ChainlinkTestnet = artifacts.require("ChainlinkTestnet.sol") + +const ZERO = toBN('0') +const ZERO_ADDRESS = th.ZERO_ADDRESS +const maxBytes32 = th.maxBytes32 + +const getFrontEndTag = async (stabilityPool, depositor) => { + return (await stabilityPool.deposits(depositor))[1] +} + +contract('Pickle', async accounts => { + const [owner, + defaulter_1, defaulter_2, defaulter_3, + whale, + alice, bob, carol, dennis, erin, flyn, + A, B, C, D, E, F, + u1, u2, u3, u4, u5, + v1, v2, v3, v4, v5, + frontEnd_1, frontEnd_2, frontEnd_3, + bammOwner + ] = accounts; + + const [bountyAddress, lpRewardsAddress, multisig] = accounts.slice(997, 1000) + + const frontEnds = [frontEnd_1, frontEnd_2, frontEnd_3] + let contracts + let priceFeed + let lusdToken + let sortedTroves + let troveManager + let activePool + let stabilityPool + let bamm + let chainlink + let defaultPool + let borrowerOperations + let lqtyToken + let communityIssuance + + let gasPriceInWei + let pLqty + let pJar + + const feePool = "0x1000000000000000000000000000000000000001" + + const getOpenTroveLUSDAmount = async (totalDebt) => th.getOpenTroveLUSDAmount(contracts, totalDebt) + const openTrove = async (params) => th.openTrove(contracts, params) + //const assertRevert = th.assertRevert + + describe("PBAMM", async () => { + + before(async () => { + gasPriceInWei = await web3.eth.getGasPrice() + }) + + beforeEach(async () => { + contracts = await deploymentHelper.deployLiquityCore() + contracts.troveManager = await TroveManagerTester.new() + contracts.lusdToken = await LUSDToken.new( + contracts.troveManager.address, + contracts.stabilityPool.address, + contracts.borrowerOperations.address + ) + const LQTYContracts = await deploymentHelper.deployLQTYContracts(bountyAddress, lpRewardsAddress, multisig) + + priceFeed = contracts.priceFeedTestnet + lusdToken = contracts.lusdToken + sortedTroves = contracts.sortedTroves + troveManager = contracts.troveManager + activePool = contracts.activePool + stabilityPool = contracts.stabilityPool + defaultPool = contracts.defaultPool + borrowerOperations = contracts.borrowerOperations + hintHelpers = contracts.hintHelpers + + lqtyToken = LQTYContracts.lqtyToken + communityIssuance = LQTYContracts.communityIssuance + + await deploymentHelper.connectLQTYContracts(LQTYContracts) + await deploymentHelper.connectCoreContracts(contracts, LQTYContracts) + await deploymentHelper.connectLQTYContractsToCore(LQTYContracts, contracts) + + // Register 3 front ends + //await th.registerFrontEnds(frontEnds, stabilityPool) + + // deploy BAMM + chainlink = await ChainlinkTestnet.new(priceFeed.address) + + const kickbackRate_F1 = toBN(dec(5, 17)) // F1 kicks 50% back to depositor + await stabilityPool.registerFrontEnd(kickbackRate_F1, { from: frontEnd_1 }) + + pLqty = await EIP20.new(0, "pickle lqty", 18, "pLqty") + pJar = await Pickle.new(lqtyToken.address, pLqty.address) + bamm = await BAMM.new(chainlink.address, stabilityPool.address, lusdToken.address, lqtyToken.address, 400, feePool, frontEnd_1, + pLqty.address, pJar.address, {from: bammOwner}) + }) + + // --- provideToSP() --- + // increases recorded LUSD at Stability Pool + it("deposit(): increases the Stability Pool LUSD balance", async () => { + // --- SETUP --- Give Alice a least 200 + await openTrove({ extraLUSDAmount: toBN(200), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + + // --- TEST --- + await lusdToken.approve(bamm.address, toBN(200), { from: alice }) + await bamm.deposit(toBN(200), { from: alice }) + + // check LUSD balances after + const stabilityPool_LUSD_After = await stabilityPool.getTotalLUSDDeposits() + assert.equal(stabilityPool_LUSD_After, 200) + }) + + // --- provideToSP() --- + // increases recorded LUSD at Stability Pool + it("deposit(): two users deposit, check their share", async () => { + // --- SETUP --- Give Alice a least 200 + await openTrove({ extraLUSDAmount: toBN(200), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await openTrove({ extraLUSDAmount: toBN(200), ICR: toBN(dec(2, 18)), extraParams: { from: whale } }) + + // --- TEST --- + await lusdToken.approve(bamm.address, toBN(200), { from: alice }) + await lusdToken.approve(bamm.address, toBN(200), { from: whale }) + await bamm.deposit(toBN(200), { from: alice }) + await bamm.deposit(toBN(200), { from: whale }) + + // check LUSD balances after1 + const whaleShare = await bamm.stake(whale) + const aliceShare = await bamm.stake(alice) + + assert.equal(whaleShare.toString(), aliceShare.toString()) + }) + + // --- provideToSP() --- + // increases recorded LUSD at Stability Pool + it("deposit(): two users deposit, one withdraw. check their share", async () => { + // --- SETUP --- Give Alice a least 200 + await openTrove({ extraLUSDAmount: toBN(200), ICR: toBN(dec(2, 18)), extraParams: { from: alice } }) + await openTrove({ extraLUSDAmount: toBN(200), ICR: toBN(dec(2, 18)), extraParams: { from: whale } }) + + // --- TEST --- + await lusdToken.approve(bamm.address, toBN(200), { from: alice }) + await lusdToken.approve(bamm.address, toBN(100), { from: whale }) + await bamm.deposit(toBN(200), { from: alice }) + await bamm.deposit(toBN(100), { from: whale }) + + // check LUSD balances after1 + const whaleShare = await bamm.stake(whale) + const aliceShare = await bamm.stake(alice) + + assert.equal(whaleShare.mul(toBN(2)).toString(), aliceShare.toString()) + + const whaleBalanceBefore = await lusdToken.balanceOf(whale) + const shareToWithdraw = whaleShare.div(toBN(2)); + await bamm.withdraw(shareToWithdraw, { from: whale }); + + const newWhaleShare = await bamm.stake(whale) + assert.equal(newWhaleShare.mul(toBN(2)).toString(), whaleShare.toString()) + + const whaleBalanceAfter = await lusdToken.balanceOf(whale) + assert.equal(whaleBalanceAfter.sub(whaleBalanceBefore).toString(), 50) + }) + + it('rebalance scenario', async () => { + // --- SETUP --- + + // Whale opens Trove and deposits to SP + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + const whaleLUSD = await lusdToken.balanceOf(whale) + await lusdToken.approve(bamm.address, whaleLUSD, { from: whale }) + bamm.deposit(whaleLUSD, { from: whale } ) + + // 2 Troves opened, each withdraws minimum debt + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1, } }) + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2, } }) + + // Alice makes Trove and withdraws 100 LUSD + await openTrove({ extraLUSDAmount: toBN(dec(100, 18)), ICR: toBN(dec(5, 18)), extraParams: { from: alice, value: dec(50, 'ether') } }) + + + // price drops: defaulter's Troves fall below MCR, whale doesn't + await priceFeed.setPrice(dec(105, 18)); + console.log("rebalance", (await bamm.fetchPrice()).toString()) + + const SPLUSD_Before = await stabilityPool.getTotalLUSDDeposits() + + // Troves are closed + await troveManager.liquidate(defaulter_1, { from: owner }) + await troveManager.liquidate(defaulter_2, { from: owner }) + + // Confirm SP has decreased + const SPLUSD_After = await stabilityPool.getTotalLUSDDeposits() + assert.isTrue(SPLUSD_After.lt(SPLUSD_Before)) + + console.log((await stabilityPool.getCompoundedLUSDDeposit(bamm.address)).toString()) + console.log((await stabilityPool.getDepositorETHGain(bamm.address)).toString()) + const price = await priceFeed.fetchPrice.call() + console.log(price.toString()) + + const ammExpectedEth = await bamm.getSwapEthAmount.call(toBN(dec(1, 18))) + + console.log("expected eth amount", ammExpectedEth.ethAmount.toString()) + + const rate = await bamm.getConversionRate(lusdToken.address, "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", toBN(dec(1, 18)), 0) + assert.equal(rate.toString(), ammExpectedEth.ethAmount.toString()) + + await lusdToken.approve(bamm.address, toBN(dec(1, 18)), { from: alice }) + + const dest = "0xe1A587Ac322da1611DF55b11A6bC8c6052D896cE" // dummy address + //await bamm.swap(toBN(dec(1, 18)), dest, { from: alice }) + await bamm.trade(lusdToken.address, toBN(dec(1, 18)), "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", dest, rate, true, { from: alice }); + + const swapBalance = await web3.eth.getBalance(dest) + + assert.equal(swapBalance, ammExpectedEth.ethAmount) + }) + + it("test basic LQTY allocation", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C, open troves + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: E } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: F } }) + + // D, E provide to bamm, F provide to SP + await lusdToken.approve(bamm.address, dec(1000, 18), { from: D }) + await lusdToken.approve(bamm.address, dec(2000, 18), { from: E }) + await bamm.deposit(dec(1000, 18), { from: D }) + await bamm.deposit(dec(2000, 18), { from: E }) + await stabilityPool.provideToSP(dec(3000, 18), frontEnd_1, { from: F }) + + // Get F1, F2, F3 LQTY balances before, and confirm they're zero + const D_LQTYBalance_Before = await pLqty.balanceOf(D) + const E_LQTYBalance_Before = await pLqty.balanceOf(E) + const F_LQTYBalance_Before = await lqtyToken.balanceOf(F) + + assert.equal(D_LQTYBalance_Before, '0') + assert.equal(E_LQTYBalance_Before, '0') + assert.equal(F_LQTYBalance_Before, '0') + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + await stabilityPool.withdrawFromSP(0, { from: F }) + await bamm.withdraw(0, { from: D }) + await bamm.withdraw(0, { from: E }) + + // Get F1, F2, F3 LQTY balances after, and confirm they have increased + const D_LQTYBalance_After = await pLqty.balanceOf(D) + const E_LQTYBalance_After = await pLqty.balanceOf(E) + const F_LQTYBalance_After = await lqtyToken.balanceOf(F) + + assert((await lqtyToken.balanceOf(frontEnd_1)).gt(toBN(0))) + assert((await pLqty.balanceOf(D)).gt(toBN(0))) + assert((await pLqty.balanceOf(E)).gt(toBN(0))) + + assert.equal(D_LQTYBalance_After.add(E_LQTYBalance_After).toString(), F_LQTYBalance_After.div(toBN(2)).toString()) + }) + + it("test share + LQTY fuzzy", async () => { + const ammUsers = [u1, u2, u3, u4, u5] + const userBalance = [0, 0, 0, 0, 0] + const nonAmmUsers = [v1, v2, v3, v4, v5] + + let totalDeposits = 0 + + // test almost equal + assert(almostTheSame(web3.utils.toWei("9999"), web3.utils.toWei("9999"))) + assert(! almostTheSame(web3.utils.toWei("9989"), web3.utils.toWei("9999"))) + + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + for(let i = 0 ; i < ammUsers.length ; i++) { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: ammUsers[i] } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: nonAmmUsers[i] } }) + + await lusdToken.approve(bamm.address, dec(1000000, 18), { from: ammUsers[i] }) + + const qty = toBN(20000) + totalDeposits += Number(qty.toString()) + userBalance[i] += Number(qty.toString()) + await bamm.deposit(qty, { from: ammUsers[i] }) + await stabilityPool.provideToSP(qty, frontEnd_1, { from: nonAmmUsers[i] }) + } + + for(n = 0 ; n < 10 ; n++) { + for(let i = 0 ; i < ammUsers.length ; i++) { + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR * (i + n + 1), web3.currentProvider) + assert(almostTheSame((await pLqty.balanceOf(ammUsers[i])).toString(), (await lqtyToken.balanceOf(nonAmmUsers[i])).div(toBN(2)).toString())) + assert.equal((await lusdToken.balanceOf(ammUsers[i])).toString(), (await lusdToken.balanceOf(nonAmmUsers[i])).toString()) + + const qty = (i+1) * 1000 + (n+1)*1000 // small number as 0 decimals + if((n*7 + i*3) % 2 === 0) { + const share = (await bamm.total()).mul(toBN(qty)).div(toBN(totalDeposits)) + console.log("withdraw", i, {qty}, {totalDeposits}, share.toString()) + await bamm.withdraw(share.toString(), { from: ammUsers[i] }) + await stabilityPool.withdrawFromSP(qty, { from: nonAmmUsers[i] }) + + totalDeposits -= qty + userBalance[i] -= qty + } + else { + console.log("deposit", i) + await bamm.deposit(qty, { from: ammUsers[i]} ) + await stabilityPool.provideToSP(qty, frontEnd_1, { from: nonAmmUsers[i] }) + + totalDeposits += qty + userBalance[i] += qty + } + + const totalSupply = await bamm.totalSupply() + const userSupply = await bamm.balanceOf(ammUsers[i]) + // userSup / totalSupply = userBalance / totalDeposits + assert.equal(userSupply.mul(toBN(totalDeposits)).toString(), toBN(userBalance[i]).mul(totalSupply).toString()) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR * (i + n + 1), web3.currentProvider) + + await bamm.withdraw(0, { from: ammUsers[i] }) + await stabilityPool.withdrawFromSP(0, { from: nonAmmUsers[i] }) + + await bamm.withdraw(0, { from: ammUsers[0] }) + await stabilityPool.withdrawFromSP(0, { from: nonAmmUsers[0] }) + + assert.equal((await lusdToken.balanceOf(ammUsers[i])).toString(), (await lusdToken.balanceOf(nonAmmUsers[i])).toString()) + assert(almostTheSame((await pLqty.balanceOf(ammUsers[i])).toString(), (await lqtyToken.balanceOf(nonAmmUsers[i])).div(toBN(2)).toString())) + assert(almostTheSame((await pLqty.balanceOf(ammUsers[0])).toString(), (await lqtyToken.balanceOf(nonAmmUsers[0])).div(toBN(2)).toString())) + } + } + + console.log("get all lqty") + for(let i = 0 ; i < ammUsers.length ; i++) { + await bamm.withdraw(0, { from: ammUsers[i] }) + await stabilityPool.withdrawFromSP(0, { from: nonAmmUsers[i] }) + } + + for(let i = 0 ; i < ammUsers.length ; i++) { + assert(almostTheSame((await pLqty.balanceOf(ammUsers[i])).toString(), (await lqtyToken.balanceOf(nonAmmUsers[i])).div(toBN(2)).toString())) + } + }) + + it("test complex LQTY allocation", async () => { + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(10, 18)), extraParams: { from: whale } }) + + // A, B, C, open troves + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: B } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: C } }) + await openTrove({ extraLUSDAmount: toBN(dec(1000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: D } }) + await openTrove({ extraLUSDAmount: toBN(dec(2000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: E } }) + await openTrove({ extraLUSDAmount: toBN(dec(3000, 18)), ICR: toBN(dec(2, 18)), extraParams: { from: F } }) + + const A_LQTYBalance_Before = await lqtyToken.balanceOf(A) + const D_LQTYBalance_Before = await lqtyToken.balanceOf(D) + const E_LQTYBalance_Before = await pLqty.balanceOf(E) + const F_LQTYBalance_Before = await pLqty.balanceOf(F) + + assert.equal(A_LQTYBalance_Before, '0') + assert.equal(D_LQTYBalance_Before, '0') + assert.equal(E_LQTYBalance_Before, '0') + assert.equal(F_LQTYBalance_Before, '0') + + // D, E provide to bamm, F provide to SP + await lusdToken.approve(bamm.address, dec(1000, 18), { from: D }) + await lusdToken.approve(bamm.address, dec(2000, 18), { from: E }) + await lusdToken.approve(bamm.address, dec(3000, 18), { from: F }) + + await bamm.deposit(dec(1000, 18), { from: D }) + await bamm.deposit(dec(2000, 18), { from: E }) + //await bamm.deposit(dec(3000, 18), { from: F }) + + await bamm.withdraw(0, { from: D }) + console.log((await lqtyToken.balanceOf(D)).toString()) + + console.log("share:", (await bamm.share.call()).toString()) + console.log("stake D:", (await bamm.stake(D)).toString()) + console.log("stake E:", (await bamm.stake(E)).toString()) + + await stabilityPool.provideToSP(dec(1000, 18), frontEnd_1, { from: A }) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + await bamm.deposit(dec(3000, 18), { from: F }) + await stabilityPool.provideToSP(dec(3000, 18), frontEnd_1, { from: B }) + + await stabilityPool.withdrawFromSP(0, { from: A }) + console.log("lqty A", (await lqtyToken.balanceOf(A)).toString()) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + console.log("share:", (await bamm.share()).toString()) + console.log("stake D:", (await bamm.stake(D)).toString()) + console.log("stake E:", (await bamm.stake(E)).toString()) + console.log("stake F:", (await bamm.stake(F)).toString()) + + await stabilityPool.withdrawFromSP(0, { from: A }) + console.log("lqty A", (await lqtyToken.balanceOf(A)).toString()) + + await stabilityPool.withdrawFromSP(0, { from: A }) + await stabilityPool.withdrawFromSP(0, { from: B }) + await bamm.withdraw(0, { from: D }) + await bamm.withdraw(0, { from: E }) + await bamm.withdraw(0, { from: F }) + + console.log("lqty D", (await pLqty.balanceOf(D)).toString()) + console.log("lqty E", (await pLqty.balanceOf(E)).toString()) + console.log("lqty F", (await pLqty.balanceOf(F)).toString()) + + console.log("share:", (await bamm.share()).toString()) + console.log("stake D:", (await bamm.stake(D)).toString()) + console.log("stake E:", (await bamm.stake(E)).toString()) + console.log("stake F:", (await bamm.stake(F)).toString()) + + // Get F1, F2, F3 LQTY balances after, and confirm they have increased + const A_LQTYBalance_After = await lqtyToken.balanceOf(A) + const B_LQTYBalance_After = await lqtyToken.balanceOf(B) + const D_LQTYBalance_After = await pLqty.balanceOf(D) + const E_LQTYBalance_After = await pLqty.balanceOf(E) + const F_LQTYBalance_After = await pLqty.balanceOf(F) + + assert.equal(D_LQTYBalance_After.toString(), A_LQTYBalance_After.div(toBN(2)).toString()) + assert.equal(E_LQTYBalance_After.toString(), A_LQTYBalance_After.div(toBN(2)).mul(toBN(2)).toString()) + assert.equal(F_LQTYBalance_After.toString(), B_LQTYBalance_After.div(toBN(2)).toString()) + }) + + it('test share with ether', async () => { + // --- SETUP --- + + // Whale opens Trove and deposits to SP + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: B } }) + + const whaleLUSD = await lusdToken.balanceOf(whale) + await lusdToken.approve(bamm.address, whaleLUSD, { from: whale }) + await lusdToken.approve(bamm.address, toBN(dec(10000, 18)), { from: A }) + await bamm.deposit(toBN(dec(10000, 18)), { from: A } ) + + // 2 Troves opened, each withdraws minimum debt + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1, } }) + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2, } }) + + + // price drops: defaulter's Troves fall below MCR, whale doesn't + await priceFeed.setPrice(dec(105, 18)); + + // Troves are closed + await troveManager.liquidate(defaulter_1, { from: owner }) + await troveManager.liquidate(defaulter_2, { from: owner }) + + // 4k liquidations + assert.equal(toBN(dec(6000, 18)).toString(), (await stabilityPool.getCompoundedLUSDDeposit(bamm.address)).toString()) + const ethGains = web3.utils.toBN("39799999999999999975") + //console.log(ethGains.toString(), (await stabilityPool.getDepositorETHGain(bamm.address)).toString()) + + // send some ETH to simulate partial rebalance + await web3.eth.sendTransaction({from: whale, to: bamm.address, value: toBN(dec(1, 18))}) + assert.equal(toBN(await web3.eth.getBalance(bamm.address)).toString(), toBN(dec(1, 18)).toString()) + + const totalEth = ethGains.add(toBN(dec(1, 18))) + const totalUsd = toBN(dec(6000, 18)).add(totalEth.mul(toBN(105))) + + await lusdToken.approve(bamm.address, totalUsd, { from: B }) + await bamm.deposit(totalUsd, { from: B } ) + + assert.equal((await bamm.balanceOf(A)).toString(), (await bamm.balanceOf(B)).toString()) + + const ethBalanceBefore = toBN(await web3.eth.getBalance(A)) + const LUSDBefore = await lusdToken.balanceOf(A) + await bamm.withdraw(await bamm.balanceOf(A), {from: A, gasPrice: 0}) + const ethBalanceAfter = toBN(await web3.eth.getBalance(A)) + const LUSDAfter = await lusdToken.balanceOf(A) + + const withdrawUsdValue = LUSDAfter.sub(LUSDBefore).add((ethBalanceAfter.sub(ethBalanceBefore)).mul(toBN(105))) + assert(in100WeiRadius(withdrawUsdValue.toString(), totalUsd.toString())) + + assert(in100WeiRadius("10283999999999999997375", "10283999999999999997322")) + assert(! in100WeiRadius("10283999999999999996375", "10283999999999999997322")) + }) + + it('price exceed max dicount and/or eth balance', async () => { + // --- SETUP --- + + // Whale opens Trove and deposits to SP + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: B } }) + + const whaleLUSD = await lusdToken.balanceOf(whale) + await lusdToken.approve(bamm.address, whaleLUSD, { from: whale }) + await lusdToken.approve(bamm.address, toBN(dec(10000, 18)), { from: A }) + await bamm.deposit(toBN(dec(10000, 18)), { from: A } ) + + // 2 Troves opened, each withdraws minimum debt + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1, } }) + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2, } }) + + + // price drops: defaulter's Troves fall below MCR, whale doesn't + await priceFeed.setPrice(dec(105, 18)); + + // Troves are closed + await troveManager.liquidate(defaulter_1, { from: owner }) + await troveManager.liquidate(defaulter_2, { from: owner }) + + // 4k liquidations + assert.equal(toBN(dec(6000, 18)).toString(), (await stabilityPool.getCompoundedLUSDDeposit(bamm.address)).toString()) + const ethGains = web3.utils.toBN("39799999999999999975") + + // without fee + await bamm.setParams(20, 0, {from: bammOwner}) + const price = await bamm.getSwapEthAmount(dec(105, 18)) + assert.equal(price.ethAmount.toString(), dec(104, 18-2).toString()) + + // with fee + await bamm.setParams(20, 100, {from: bammOwner}) + const priceWithFee = await bamm.getSwapEthAmount(dec(105, 18)) + assert.equal(priceWithFee.ethAmount.toString(), dec(10296, 18-4).toString()) + + // without fee + await bamm.setParams(20, 0, {from: bammOwner}) + const priceDepleted = await bamm.getSwapEthAmount(dec(1050000000000000, 18)) + assert.equal(priceDepleted.ethAmount.toString(), ethGains.toString()) + + // with fee + await bamm.setParams(20, 100, {from: bammOwner}) + const priceDepletedWithFee = await bamm.getSwapEthAmount(dec(1050000000000000, 18)) + assert.equal(priceDepletedWithFee.ethAmount.toString(), ethGains.mul(toBN(99)).div(toBN(100))) + }) + + it('test getSwapEthAmount', async () => { + // --- SETUP --- + + // Whale opens Trove and deposits to SP + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: B } }) + + const whaleLUSD = await lusdToken.balanceOf(whale) + await lusdToken.approve(bamm.address, whaleLUSD, { from: whale }) + await lusdToken.approve(bamm.address, toBN(dec(10000, 18)), { from: A }) + await bamm.deposit(toBN(dec(10000, 18)), { from: A } ) + + // 2 Troves opened, each withdraws minimum debt + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1, } }) + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2, } }) + + + // price drops: defaulter's Troves fall below MCR, whale doesn't + await priceFeed.setPrice(dec(105, 18)); + + // Troves are closed + await troveManager.liquidate(defaulter_1, { from: owner }) + await troveManager.liquidate(defaulter_2, { from: owner }) + + // 4k liquidations + assert.equal(toBN(dec(6000, 18)).toString(), (await stabilityPool.getCompoundedLUSDDeposit(bamm.address)).toString()) + const ethGains = web3.utils.toBN("39799999999999999975") + + const lusdQty = dec(105, 18) + const expectedReturn = await bamm.getReturn(lusdQty, dec(6000, 18), toBN(dec(6000, 18)).add(ethGains.mul(toBN(2 * 105))), 200) + + // without fee + await bamm.setParams(200, 0, {from: bammOwner}) + const priceWithoutFee = await bamm.getSwapEthAmount(lusdQty) + assert.equal(priceWithoutFee.ethAmount.toString(), expectedReturn.mul(toBN(100)).div(toBN(100 * 105)).toString()) + + // with fee + await bamm.setParams(200, 100, {from: bammOwner}) + const priceWithFee = await bamm.getSwapEthAmount(lusdQty) + assert.equal(priceWithFee.ethAmount.toString(), expectedReturn.mul(toBN(99)).div(toBN(100 * 105)).toString()) + }) + + it('test fetch price', async () => { + await priceFeed.setPrice(dec(666, 18)); + assert.equal(await bamm.fetchPrice(), dec(666, 18)) + + await chainlink.setTimestamp(888) + assert.equal((await bamm.fetchPrice()).toString(), "0") + }) + + it('test swap', async () => { + // --- SETUP --- + + // Whale opens Trove and deposits to SP + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: B } }) + + const whaleLUSD = await lusdToken.balanceOf(whale) + await lusdToken.approve(bamm.address, whaleLUSD, { from: whale }) + await lusdToken.approve(bamm.address, toBN(dec(10000, 18)), { from: A }) + await bamm.deposit(toBN(dec(10000, 18)), { from: A } ) + + // 2 Troves opened, each withdraws minimum debt + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1, } }) + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2, } }) + + + // price drops: defaulter's Troves fall below MCR, whale doesn't + await priceFeed.setPrice(dec(105, 18)); + + // Troves are closed + await troveManager.liquidate(defaulter_1, { from: owner }) + await troveManager.liquidate(defaulter_2, { from: owner }) + + // 4k liquidations + assert.equal(toBN(dec(6000, 18)).toString(), (await stabilityPool.getCompoundedLUSDDeposit(bamm.address)).toString()) + const ethGains = web3.utils.toBN("39799999999999999975") + + // with fee + await bamm.setParams(20, 100, {from: bammOwner}) + const priceWithFee = await bamm.getSwapEthAmount(dec(105, 18)) + assert.equal(priceWithFee.ethAmount.toString(), dec(10296, 18-4).toString()) + assert.equal(priceWithFee.feeEthAmount.toString(), dec(10400 - 10296, 18-4).toString()) + + await lusdToken.approve(bamm.address, dec(1,18), {from: whale}) + const dest = "0xdEADBEEF00AA81bBCF694bC5c05A397F5E5658D5" + await bamm.swap(dec(1,18), dest, {from: whale}) + + // check lusd balance + assert.equal(toBN(dec(6001, 18)).toString(), (await stabilityPool.getCompoundedLUSDDeposit(bamm.address)).toString()) + + // check eth balance + assert(await web3.eth.getBalance(dest), priceWithFee.ethAmount) + + // check fees + assert(await web3.eth.getBalance(feePool), priceWithFee.feeEthAmount) + }) + + it('test set params happy path', async () => { + // --- SETUP --- + + // Whale opens Trove and deposits to SP + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(20000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: B } }) + + const whaleLUSD = await lusdToken.balanceOf(whale) + await lusdToken.approve(bamm.address, whaleLUSD, { from: whale }) + await lusdToken.approve(bamm.address, toBN(dec(10000, 18)), { from: A }) + await bamm.deposit(toBN(dec(10000, 18)), { from: A } ) + + // 2 Troves opened, each withdraws minimum debt + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_1, } }) + await openTrove({ extraLUSDAmount: 0, ICR: toBN(dec(2, 18)), extraParams: { from: defaulter_2, } }) + + + // price drops: defaulter's Troves fall below MCR, whale doesn't + await priceFeed.setPrice(dec(105, 18)); + + // Troves are closed + await troveManager.liquidate(defaulter_1, { from: owner }) + await troveManager.liquidate(defaulter_2, { from: owner }) + + // 4k liquidations + assert.equal(toBN(dec(6000, 18)).toString(), (await stabilityPool.getCompoundedLUSDDeposit(bamm.address)).toString()) + const ethGains = web3.utils.toBN("39799999999999999975") + + const lusdQty = dec(105, 18) + const expectedReturn200 = await bamm.getReturn(lusdQty, dec(6000, 18), toBN(dec(6000, 18)).add(ethGains.mul(toBN(2 * 105))), 200) + const expectedReturn190 = await bamm.getReturn(lusdQty, dec(6000, 18), toBN(dec(6000, 18)).add(ethGains.mul(toBN(2 * 105))), 190) + + assert(expectedReturn200.toString() !== expectedReturn190.toString()) + + // without fee + await bamm.setParams(200, 0, {from: bammOwner}) + const priceWithoutFee = await bamm.getSwapEthAmount(lusdQty) + assert.equal(priceWithoutFee.ethAmount.toString(), expectedReturn200.mul(toBN(100)).div(toBN(100 * 105)).toString()) + + // with fee + await bamm.setParams(190, 100, {from: bammOwner}) + const priceWithFee = await bamm.getSwapEthAmount(lusdQty) + assert.equal(priceWithFee.ethAmount.toString(), expectedReturn190.mul(toBN(99)).div(toBN(100 * 105)).toString()) + }) + + it('test set params sad path', async () => { + await assertRevert(bamm.setParams(210, 100, {from: bammOwner}), 'setParams: A too big') + await assertRevert(bamm.setParams(10, 100, {from: bammOwner}), 'setParams: A too small') + await assertRevert(bamm.setParams(10, 101, {from: bammOwner}), 'setParams: fee is too big') + await assertRevert(bamm.setParams(20, 100, {from: B}), 'Ownable: caller is not the owner') + }) + + it('transfer test', async () => { + // --- SETUP --- + + // Whale opens Trove and deposits to SP + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: whale, value: dec(50, 'ether') } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: A } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: C } }) + await openTrove({ extraLUSDAmount: toBN(dec(10000, 18)), ICR: toBN(dec(20, 18)), extraParams: { from: D } }) + + const whaleLUSD = await lusdToken.balanceOf(whale) + await lusdToken.approve(bamm.address, whaleLUSD, { from: whale }) + await lusdToken.approve(bamm.address, toBN(dec(10000, 18)), { from: A }) + await bamm.deposit(toBN(dec(10000, 18)), { from: A } ) + await stabilityPool.provideToSP(toBN(dec(10000, 18)), frontEnd_1, {from: C}) + + assert.equal(await bamm.balanceOf(A), dec(1, 18)) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + await stabilityPool.provideToSP(toBN(dec(5000, 18)), frontEnd_1, {from: D}) + + await bamm.transfer(B, dec(5, 17), {from: A}) + assert.equal(await bamm.balanceOf(A), dec(5, 17)) + assert.equal(await bamm.balanceOf(B), dec(5, 17)) + + await stabilityPool.withdrawFromSP(toBN(dec(5000, 18)), { from: C }) + assert.equal(await pLqty.balanceOf(B), "0") + await bamm.withdraw(0, {from: A}) + assert.equal((await pLqty.balanceOf(A)).toString(), (await lqtyToken.balanceOf(C)).div(toBN(2)).toString()) + + // reset A's usd balance + await lusdToken.transfer(C, await lusdToken.balanceOf(A), {from: A}) + assert.equal(await lusdToken.balanceOf(A), "0") + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + await bamm.withdraw(toBN(dec(5, 17)), {from: A}) // check balance + await bamm.withdraw(toBN(dec(5, 17)), {from: B}) // check balance + await stabilityPool.withdrawFromSP(toBN(dec(5000, 18)), { from: C }) + await stabilityPool.withdrawFromSP(toBN(dec(5000, 18)), { from: D }) + + assert.equal((await pLqty.balanceOf(B)).toString(), (await lqtyToken.balanceOf(D)).div(toBN(2)).toString()) + assert.equal((await pLqty.balanceOf(A)).toString(), (await lqtyToken.balanceOf(C)).div(toBN(2)).toString()) + + assert.equal((await lusdToken.balanceOf(B)).toString(), dec(5000, 18)) + assert.equal((await lusdToken.balanceOf(A)).toString(), dec(5000, 18)) + + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + + // TODO check lqty now + }) + + // tests: + // 1. complex lqty staking + share V + // 2. share test with ether V + // 3. basic share with liquidation (withdraw after liquidation) V + // 4. price that exceeds max discount V + // 5. price that exceeds balance V + // 5.5 test fees and return V + // 5.6 test swap v + // 6.1 test fetch price V + // 6. set params V + // 7. test with front end v + // 8. formula V + // 9. lp token + // 11. pickle + // 10. cleanups - compilation warnings. cropjoin - revoke changes and maybe make internal. + }) +}) + + +function almostTheSame(n1, n2) { + n1 = Number(web3.utils.fromWei(n1)) + n2 = Number(web3.utils.fromWei(n2)) + //console.log(n1,n2) + + if(n1 * 1000 > n2 * 1001) return false + if(n2 * 1000 > n1 * 1001) return false + return true +} + +function in100WeiRadius(n1, n2) { + const x = toBN(n1) + const y = toBN(n2) + + if(x.add(toBN(100)).lt(y)) return false + if(y.add(toBN(100)).lt(x)) return false + + return true +} + +async function assertRevert(txPromise, message = undefined) { + try { + const tx = await txPromise + // console.log("tx succeeded") + assert.isFalse(tx.receipt.status) // when this assert fails, the expected revert didn't occur, i.e. the tx succeeded + } catch (err) { + // console.log("tx failed") + assert.include(err.message, "revert") + + if (message) { + assert.include(err.message, message) + } + } +} \ No newline at end of file From c823767af11c1a1fd8772bacb69b8fe58bc33912 Mon Sep 17 00:00:00 2001 From: yaron velner Date: Wed, 30 Jun 2021 15:08:26 +0300 Subject: [PATCH 14/90] remove erc20 operations from bamm --- .../contracts/contracts/B.Protocol/BAMM.sol | 8 ++-- .../{LPToken.sol => CropJoinAdapter.sol} | 39 +++---------------- .../contracts/contracts/B.Protocol/crop.sol | 18 +-------- .../contracts/test/B.Protocol/BAMMTest.js | 9 ++--- .../contracts/test/B.Protocol/PickleTest.js | 2 +- 5 files changed, 18 insertions(+), 58 deletions(-) rename packages/contracts/contracts/B.Protocol/{LPToken.sol => CropJoinAdapter.sol} (51%) diff --git a/packages/contracts/contracts/B.Protocol/BAMM.sol b/packages/contracts/contracts/B.Protocol/BAMM.sol index ba427c65e..d9de4b78e 100644 --- a/packages/contracts/contracts/B.Protocol/BAMM.sol +++ b/packages/contracts/contracts/B.Protocol/BAMM.sol @@ -3,7 +3,7 @@ pragma solidity 0.6.11; import "./../StabilityPool.sol"; -import "./LPToken.sol"; +import "./CropJoinAdapter.sol"; import "./PriceFormula.sol"; import "./../Interfaces/IPriceFeed.sol"; import "./../Dependencies/IERC20.sol"; @@ -11,11 +11,12 @@ import "./../Dependencies/SafeMath.sol"; import "./../Dependencies/Ownable.sol"; import "./../Dependencies/AggregatorV3Interface.sol"; -contract BAMM is LPToken, PriceFormula, Ownable { +contract BAMM is CropJoinAdapter, PriceFormula, Ownable { using SafeMath for uint256; AggregatorV3Interface public immutable priceAggregator; IERC20 public immutable LUSD; + StabilityPool immutable public SP; address payable public immutable feePool; uint public constant MAX_FEE = 100; // 1% @@ -39,10 +40,11 @@ contract BAMM is LPToken, PriceFormula, Ownable { address payable _feePool, address _fronEndTag) public - LPToken(_LQTY, _SP) + CropJoinAdapter(_LQTY) { priceAggregator = AggregatorV3Interface(_priceAggregator); LUSD = IERC20(_LUSD); + SP = StabilityPool(_SP); feePool = _feePool; maxDiscount = _maxDiscount; diff --git a/packages/contracts/contracts/B.Protocol/LPToken.sol b/packages/contracts/contracts/B.Protocol/CropJoinAdapter.sol similarity index 51% rename from packages/contracts/contracts/B.Protocol/LPToken.sol rename to packages/contracts/contracts/B.Protocol/CropJoinAdapter.sol index bde09ad28..c8e2d3da0 100644 --- a/packages/contracts/contracts/B.Protocol/LPToken.sol +++ b/packages/contracts/contracts/B.Protocol/CropJoinAdapter.sol @@ -3,19 +3,16 @@ pragma solidity 0.6.11; import "./crop.sol"; import "./../StabilityPool.sol"; -contract LPToken is CropJoin { +// NOTE! - this is not an ERC20 token. transfer is not supported. +contract CropJoinAdapter is CropJoin { string constant public name = "B.AMM LUSD-ETH"; string constant public symbol = "LUSDETH"; uint constant public decimals = 18; - mapping(address => mapping(address => uint)) allowance; - StabilityPool immutable public SP; event Transfer(address indexed _from, address indexed _to, uint256 _value); - event Approval(address indexed _owner, address indexed _spender, uint256 _value); - constructor(address _lqty, address payable _SP) public + constructor(address _lqty) public CropJoin(address(new Dummy()), "B.AMM", address(new DummyGem()), _lqty) { - SP = StabilityPool(_SP); } function mint(address to, uint value) virtual internal { @@ -36,33 +33,9 @@ contract LPToken is CropJoin { balance = stake[owner]; } - function transfer(address to, uint256 value) public returns (bool success) { - // get all lqty to the contract - SP.withdrawFromSP(0); - - burn(msg.sender, value); - mint(to, value); - - // event is emitted in burn and mint - success = true; - } - - function transferFrom(address from, address to, uint256 value) public returns (bool success) { - // get all lqty to the contract - SP.withdrawFromSP(0); - - allowance[msg.sender][from] = sub(allowance[msg.sender][from], value); - - burn(from, value); - mint(to, value); - - // event is emitted in burn and mint - success = true; - } - - function approve(address spender, uint256 value) public returns (bool success) { - allowance[msg.sender][spender] = value; - emit Approval(msg.sender, spender, value); + // adapter to cropjoin + function nav() public override returns (uint256) { + return total; } } diff --git a/packages/contracts/contracts/B.Protocol/crop.sol b/packages/contracts/contracts/B.Protocol/crop.sol index 7d43ec40e..8eb279ef0 100644 --- a/packages/contracts/contracts/B.Protocol/crop.sol +++ b/packages/contracts/contracts/B.Protocol/crop.sol @@ -104,7 +104,8 @@ contract CropJoin { // Net Asset Valuation [wad] function nav() public virtual returns (uint256) { - return total; + uint256 _nav = gem.balanceOf(address(this)); + return mul(_nav, to18ConversionFactor); } // Net Assets per Share [wad] @@ -163,19 +164,4 @@ contract CropJoin { crops[msg.sender] = rmulup(stake[msg.sender], share); emit Exit(val); } - - function flee() internal virtual { - uint256 wad = vat.gem(ilk, msg.sender); - require(wad <= 2 ** 255); - uint256 val = wmul(wmul(wad, nps()), toGemConversionFactor); - - require(gem.transfer(msg.sender, val)); - vat.slip(ilk, msg.sender, -int256(wad)); - - total = sub(total, wad); - stake[msg.sender] = sub(stake[msg.sender], wad); - crops[msg.sender] = rmulup(stake[msg.sender], share); - - emit Flee(); - } } diff --git a/packages/contracts/test/B.Protocol/BAMMTest.js b/packages/contracts/test/B.Protocol/BAMMTest.js index 5e440f7d0..b2dfa99d7 100644 --- a/packages/contracts/test/B.Protocol/BAMMTest.js +++ b/packages/contracts/test/B.Protocol/BAMMTest.js @@ -686,7 +686,7 @@ contract('BAMM', async accounts => { await assertRevert(bamm.setParams(20, 100, {from: B}), 'Ownable: caller is not the owner') }) - it('transfer test', async () => { + it.skip('transfer happy test', async () => { // transfer is not supported anymore // --- SETUP --- // Whale opens Trove and deposits to SP @@ -732,9 +732,8 @@ contract('BAMM', async accounts => { assert.equal((await lusdToken.balanceOf(B)).toString(), dec(5000, 18)) assert.equal((await lusdToken.balanceOf(A)).toString(), dec(5000, 18)) + }) - await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) - }) // tests: // 1. complex lqty staking + share V @@ -748,8 +747,8 @@ contract('BAMM', async accounts => { // 6. set params V // 7. test with front end v // 8. formula V - // 9. lp token - // 11. pickle + // 9. lp token - transfer sad test + // 11. pickle V // 10. cleanups - compilation warnings. cropjoin - revoke changes and maybe make internal. }) }) diff --git a/packages/contracts/test/B.Protocol/PickleTest.js b/packages/contracts/test/B.Protocol/PickleTest.js index 3c270a39b..a24b978f0 100644 --- a/packages/contracts/test/B.Protocol/PickleTest.js +++ b/packages/contracts/test/B.Protocol/PickleTest.js @@ -695,7 +695,7 @@ contract('Pickle', async accounts => { await assertRevert(bamm.setParams(20, 100, {from: B}), 'Ownable: caller is not the owner') }) - it('transfer test', async () => { + it.skip('transfer happy test', async () => { // transfer is not supported anymore // --- SETUP --- // Whale opens Trove and deposits to SP From 78592596752b02b045ab08ae64f3c93c36eb035f Mon Sep 17 00:00:00 2001 From: yaron velner Date: Wed, 30 Jun 2021 15:36:40 +0300 Subject: [PATCH 15/90] compiler warning cleanups --- packages/contracts/contracts/B.Protocol/BAMM.sol | 14 +++++++------- .../contracts/B.Protocol/ChainlinkTestnet.sol | 6 +++--- .../contracts/B.Protocol/CropJoinAdapter.sol | 4 +++- .../contracts/B.Protocol/PriceFormula.sol | 2 ++ 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/contracts/contracts/B.Protocol/BAMM.sol b/packages/contracts/contracts/B.Protocol/BAMM.sol index d9de4b78e..c62a24053 100644 --- a/packages/contracts/contracts/B.Protocol/BAMM.sol +++ b/packages/contracts/contracts/B.Protocol/BAMM.sol @@ -186,21 +186,21 @@ contract BAMM is CropJoinAdapter, PriceFormula, Ownable { // kyber network reserve compatible function function trade( - IERC20 srcToken, + IERC20 /* srcToken */, uint256 srcAmount, - IERC20 destToken, + IERC20 /* destToken */, address payable destAddress, - uint256 conversionRate, - bool validate + uint256 /* conversionRate */, + bool /* validate */ ) external payable returns (bool) { return swap(srcAmount, destAddress) > 0; } function getConversionRate( - IERC20 src, - IERC20 dest, + IERC20 /* src */, + IERC20 /* dest */, uint256 srcQty, - uint256 blockNumber + uint256 /* blockNumber */ ) external view returns (uint256) { (uint ethQty, ) = getSwapEthAmount(srcQty); return ethQty.mul(PRECISION) / srcQty; diff --git a/packages/contracts/contracts/B.Protocol/ChainlinkTestnet.sol b/packages/contracts/contracts/B.Protocol/ChainlinkTestnet.sol index 74a54d6a7..c956fe1f3 100644 --- a/packages/contracts/contracts/B.Protocol/ChainlinkTestnet.sol +++ b/packages/contracts/contracts/B.Protocol/ChainlinkTestnet.sol @@ -27,11 +27,11 @@ contract ChainlinkTestnet { function latestRoundData() external view returns ( - uint80 roundId, + uint80 /* roundId */, int256 answer, - uint256 startedAt, + uint256 /* startedAt */, uint256 timestamp, - uint80 answeredInRound + uint80 /* answeredInRound */ ) { answer = int(feed.getPrice()); diff --git a/packages/contracts/contracts/B.Protocol/CropJoinAdapter.sol b/packages/contracts/contracts/B.Protocol/CropJoinAdapter.sol index c8e2d3da0..04efae495 100644 --- a/packages/contracts/contracts/B.Protocol/CropJoinAdapter.sol +++ b/packages/contracts/contracts/B.Protocol/CropJoinAdapter.sol @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: MIT + pragma solidity 0.6.11; import "./crop.sol"; @@ -40,7 +42,7 @@ contract CropJoinAdapter is CropJoin { } contract Dummy { - fallback() external payable {} + fallback() external {} } contract DummyGem is Dummy { diff --git a/packages/contracts/contracts/B.Protocol/PriceFormula.sol b/packages/contracts/contracts/B.Protocol/PriceFormula.sol index b685ad041..14b801227 100644 --- a/packages/contracts/contracts/B.Protocol/PriceFormula.sol +++ b/packages/contracts/contracts/B.Protocol/PriceFormula.sol @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: MIT + pragma solidity 0.6.11; import "./../Dependencies/SafeMath.sol"; From 6a63feb22489e52fb8dd06c6ff93c5b546c93647 Mon Sep 17 00:00:00 2001 From: yaron velner Date: Wed, 30 Jun 2021 19:41:55 +0300 Subject: [PATCH 16/90] events --- packages/contracts/contracts/B.Protocol/BAMM.sol | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/contracts/contracts/B.Protocol/BAMM.sol b/packages/contracts/contracts/B.Protocol/BAMM.sol index c62a24053..9db3bfa6d 100644 --- a/packages/contracts/contracts/B.Protocol/BAMM.sol +++ b/packages/contracts/contracts/B.Protocol/BAMM.sol @@ -31,6 +31,11 @@ contract BAMM is CropJoinAdapter, PriceFormula, Ownable { uint constant PRECISION = 1e18; + event ParamsSet(uint A, uint fee); + event UserDeposit(address indexed user, uint lusdAmount, uint numShares); + event UserWithdraw(address indexed user, uint lusdAmount, uint ethAmount, uint numShares); + event RebalanceSwap(address indexed user, uint lusdAmount, uint ethAmount, uint timestamp); + constructor( address _priceAggregator, address payable _SP, @@ -57,7 +62,9 @@ contract BAMM is CropJoinAdapter, PriceFormula, Ownable { require(_A <= MAX_A, "setParams: A too big"); fee = _fee; - A = _A; + A = _A; + + emit ParamsSet(_A, _fee); } function fetchPrice() public view returns(uint) { @@ -117,6 +124,8 @@ contract BAMM is CropJoinAdapter, PriceFormula, Ownable { // update LP token mint(msg.sender, newShare); + + emit UserDeposit(msg.sender, lusdAmount, newShare); } function withdraw(uint numShares) external { @@ -138,6 +147,8 @@ contract BAMM is CropJoinAdapter, PriceFormula, Ownable { (bool success, ) = msg.sender.call{ value: ethAmount }(""); // re-entry is fine here require(success, "withdraw: sending ETH failed"); } + + emit UserWithdraw(msg.sender, lusdAmount, ethAmount, numShares); } function addBps(uint n, int bps) internal pure returns(uint) { @@ -181,6 +192,8 @@ contract BAMM is CropJoinAdapter, PriceFormula, Ownable { (bool success, ) = dest.call{ value: ethAmount }(""); // re-entry is fine here require(success, "swap: sending ETH failed"); + emit RebalanceSwap(msg.sender, lusdAmount, ethAmount, now); + return ethAmount; } From 3af7bcf97f3bcc35c017f54672fa1f4c6e0dfd14 Mon Sep 17 00:00:00 2001 From: yaron velner Date: Wed, 30 Jun 2021 20:13:22 +0300 Subject: [PATCH 17/90] linter --- packages/contracts/contracts/B.Protocol/BAMM.sol | 7 ++++--- packages/contracts/contracts/B.Protocol/PBAMM.sol | 2 +- packages/contracts/test/B.Protocol/BAMMTest.js | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/contracts/contracts/B.Protocol/BAMM.sol b/packages/contracts/contracts/B.Protocol/BAMM.sol index 9db3bfa6d..312899c75 100644 --- a/packages/contracts/contracts/B.Protocol/BAMM.sol +++ b/packages/contracts/contracts/B.Protocol/BAMM.sol @@ -11,12 +11,13 @@ import "./../Dependencies/SafeMath.sol"; import "./../Dependencies/Ownable.sol"; import "./../Dependencies/AggregatorV3Interface.sol"; + contract BAMM is CropJoinAdapter, PriceFormula, Ownable { using SafeMath for uint256; AggregatorV3Interface public immutable priceAggregator; IERC20 public immutable LUSD; - StabilityPool immutable public SP; + StabilityPool immutable public SP; address payable public immutable feePool; uint public constant MAX_FEE = 100; // 1% @@ -29,7 +30,7 @@ contract BAMM is CropJoinAdapter, PriceFormula, Ownable { address public immutable frontEndTag; - uint constant PRECISION = 1e18; + uint constant public PRECISION = 1e18; event ParamsSet(uint A, uint fee); event UserDeposit(address indexed user, uint lusdAmount, uint numShares); @@ -219,5 +220,5 @@ contract BAMM is CropJoinAdapter, PriceFormula, Ownable { return ethQty.mul(PRECISION) / srcQty; } - receive() external payable {} + receive() external payable {} } diff --git a/packages/contracts/contracts/B.Protocol/PBAMM.sol b/packages/contracts/contracts/B.Protocol/PBAMM.sol index 4a2ba74b5..ffbdf3a2e 100644 --- a/packages/contracts/contracts/B.Protocol/PBAMM.sol +++ b/packages/contracts/contracts/B.Protocol/PBAMM.sol @@ -9,7 +9,7 @@ interface PickleJarLike { } contract PBAMM is BAMM { - PickleJarLike immutable pickleJar; + PickleJarLike public immutable pickleJar; constructor( address _priceAggregator, diff --git a/packages/contracts/test/B.Protocol/BAMMTest.js b/packages/contracts/test/B.Protocol/BAMMTest.js index b2dfa99d7..da87a8b5d 100644 --- a/packages/contracts/test/B.Protocol/BAMMTest.js +++ b/packages/contracts/test/B.Protocol/BAMMTest.js @@ -749,7 +749,8 @@ contract('BAMM', async accounts => { // 8. formula V // 9. lp token - transfer sad test // 11. pickle V - // 10. cleanups - compilation warnings. cropjoin - revoke changes and maybe make internal. + // 10. cleanups - compilation warnings. cropjoin - revoke changes and maybe make internal. V + // 12 - linter. events }) }) From 4d82e3ef011df10930064f2e11e4c2219a5e8f48 Mon Sep 17 00:00:00 2001 From: yaron velner Date: Wed, 30 Jun 2021 20:21:39 +0300 Subject: [PATCH 18/90] function order according to linter --- .../contracts/B.Protocol/CropJoinAdapter.sol | 27 ++++++++++--------- .../contracts/contracts/B.Protocol/PBAMM.sol | 12 ++++----- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/contracts/contracts/B.Protocol/CropJoinAdapter.sol b/packages/contracts/contracts/B.Protocol/CropJoinAdapter.sol index 04efae495..f2f982f74 100644 --- a/packages/contracts/contracts/B.Protocol/CropJoinAdapter.sol +++ b/packages/contracts/contracts/B.Protocol/CropJoinAdapter.sol @@ -14,19 +14,15 @@ contract CropJoinAdapter is CropJoin { event Transfer(address indexed _from, address indexed _to, uint256 _value); constructor(address _lqty) public - CropJoin(address(new Dummy()), "B.AMM", address(new DummyGem()), _lqty) { + CropJoin(address(new Dummy()), "B.AMM", address(new DummyGem()), _lqty) + { } - function mint(address to, uint value) virtual internal { - join(to, value); - emit Transfer(address(0), to, value); - } - - function burn(address owner, uint value) virtual internal { - exit(owner, value); - emit Transfer(owner, address(0), value); + // adapter to cropjoin + function nav() public override returns (uint256) { + return total; } - + function totalSupply() public view returns (uint256) { return total; } @@ -35,9 +31,14 @@ contract CropJoinAdapter is CropJoin { balance = stake[owner]; } - // adapter to cropjoin - function nav() public override returns (uint256) { - return total; + function mint(address to, uint value) virtual internal { + join(to, value); + emit Transfer(address(0), to, value); + } + + function burn(address owner, uint value) virtual internal { + exit(owner, value); + emit Transfer(owner, address(0), value); } } diff --git a/packages/contracts/contracts/B.Protocol/PBAMM.sol b/packages/contracts/contracts/B.Protocol/PBAMM.sol index ffbdf3a2e..50d2c371f 100644 --- a/packages/contracts/contracts/B.Protocol/PBAMM.sol +++ b/packages/contracts/contracts/B.Protocol/PBAMM.sol @@ -29,6 +29,12 @@ contract PBAMM is BAMM { require(IERC20(_LQTY).approve(_pickleJar, type(uint).max), "constructor: approve failed"); } + // callable by anyone + function depositLqty() external { + SP.withdrawFromSP(0); + pickleJar.depositAll(); + } + function mint(address to, uint value) override internal { pickleJar.depositAll(); super.mint(to, value); @@ -38,10 +44,4 @@ contract PBAMM is BAMM { pickleJar.depositAll(); super.burn(owner, value); } - - // callable by anyone - function depositLqty() external { - SP.withdrawFromSP(0); - pickleJar.depositAll(); - } } From ecb8e742aa39d2f29c476597c11fed7db441aacd Mon Sep 17 00:00:00 2001 From: yaron velner Date: Wed, 7 Jul 2021 17:26:58 +0300 Subject: [PATCH 19/90] audit fixes --- packages/contracts/contracts/B.Protocol/BAMM.sol | 13 ++++++++++--- .../contracts/contracts/B.Protocol/PriceFormula.sol | 2 +- packages/contracts/test/B.Protocol/BAMMTest.js | 12 +++++++----- packages/contracts/test/B.Protocol/PickleTest.js | 10 +++++----- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/contracts/contracts/B.Protocol/BAMM.sol b/packages/contracts/contracts/B.Protocol/BAMM.sol index 312899c75..3fd4dda30 100644 --- a/packages/contracts/contracts/B.Protocol/BAMM.sol +++ b/packages/contracts/contracts/B.Protocol/BAMM.sol @@ -116,8 +116,12 @@ contract BAMM is CropJoinAdapter, PriceFormula, Ownable { uint totalValue = lusdValue.add(ethValue.mul(price) / PRECISION); + // this is in theory not reachable. if it is, better halt deposits + // the condition is equivalent to: (totalValue = 0) ==> (total = 0) + require(totalValue > 0 || total == 0, "deposit: system is rekt"); + uint newShare = PRECISION; - if(totalValue > 0) newShare = total.mul(lusdAmount) / totalValue; + if(total > 0) newShare = total.mul(lusdAmount) / totalValue; // deposit require(LUSD.transferFrom(msg.sender, address(this), lusdAmount), "deposit: transferFrom failed"); @@ -184,8 +188,11 @@ contract BAMM is CropJoinAdapter, PriceFormula, Ownable { } // get ETH in return to LUSD - function swap(uint lusdAmount, address payable dest) public payable returns(uint) { + function swap(uint lusdAmount, uint minEthReturn, address payable dest) public payable returns(uint) { (uint ethAmount, uint feeAmount) = getSwapEthAmount(lusdAmount); + + require(ethAmount >= minEthReturn, "swap: low return"); + LUSD.transferFrom(msg.sender, address(this), lusdAmount); SP.provideToSP(lusdAmount, frontEndTag); @@ -207,7 +214,7 @@ contract BAMM is CropJoinAdapter, PriceFormula, Ownable { uint256 /* conversionRate */, bool /* validate */ ) external payable returns (bool) { - return swap(srcAmount, destAddress) > 0; + return swap(srcAmount, 0, destAddress) > 0; } function getConversionRate( diff --git a/packages/contracts/contracts/B.Protocol/PriceFormula.sol b/packages/contracts/contracts/B.Protocol/PriceFormula.sol index 14b801227..9bcafc6d8 100644 --- a/packages/contracts/contracts/B.Protocol/PriceFormula.sol +++ b/packages/contracts/contracts/B.Protocol/PriceFormula.sol @@ -23,7 +23,7 @@ contract PriceFormula { uint d = (A.mul(2).sub(1).mul(sum)); sum = n / d.add(dP.mul(3)); - if(sum <= prevSum.add(1) && prevSum.add(1) <= sum) break; + if(sum <= prevSum.add(1) && prevSum <= sum.add(1)) break; } return sum; diff --git a/packages/contracts/test/B.Protocol/BAMMTest.js b/packages/contracts/test/B.Protocol/BAMMTest.js index da87a8b5d..df8a028e6 100644 --- a/packages/contracts/test/B.Protocol/BAMMTest.js +++ b/packages/contracts/test/B.Protocol/BAMMTest.js @@ -619,18 +619,20 @@ contract('BAMM', async accounts => { assert.equal(priceWithFee.ethAmount.toString(), dec(10296, 18-4).toString()) assert.equal(priceWithFee.feeEthAmount.toString(), dec(10400 - 10296, 18-4).toString()) - await lusdToken.approve(bamm.address, dec(1,18), {from: whale}) + await lusdToken.approve(bamm.address, dec(105,18), {from: whale}) const dest = "0xdEADBEEF00AA81bBCF694bC5c05A397F5E5658D5" - await bamm.swap(dec(1,18), dest, {from: whale}) + + await assertRevert(bamm.swap(dec(105,18), priceWithFee.ethAmount.add(toBN(1)), dest, {from: whale}), 'swap: low return') + await bamm.swap(dec(105,18), priceWithFee.ethAmount, dest, {from: whale}) // TODO - check once with higher value so it will revert // check lusd balance - assert.equal(toBN(dec(6001, 18)).toString(), (await stabilityPool.getCompoundedLUSDDeposit(bamm.address)).toString()) + assert.equal(toBN(dec(6105, 18)).toString(), (await stabilityPool.getCompoundedLUSDDeposit(bamm.address)).toString()) // check eth balance - assert(await web3.eth.getBalance(dest), priceWithFee.ethAmount) + assert.equal(await web3.eth.getBalance(dest), priceWithFee.ethAmount) // check fees - assert(await web3.eth.getBalance(feePool), priceWithFee.feeEthAmount) + assert.equal(await web3.eth.getBalance(feePool), priceWithFee.feeEthAmount) }) it('test set params happy path', async () => { diff --git a/packages/contracts/test/B.Protocol/PickleTest.js b/packages/contracts/test/B.Protocol/PickleTest.js index a24b978f0..582e5f154 100644 --- a/packages/contracts/test/B.Protocol/PickleTest.js +++ b/packages/contracts/test/B.Protocol/PickleTest.js @@ -628,18 +628,18 @@ contract('Pickle', async accounts => { assert.equal(priceWithFee.ethAmount.toString(), dec(10296, 18-4).toString()) assert.equal(priceWithFee.feeEthAmount.toString(), dec(10400 - 10296, 18-4).toString()) - await lusdToken.approve(bamm.address, dec(1,18), {from: whale}) + await lusdToken.approve(bamm.address, dec(105,18), {from: whale}) const dest = "0xdEADBEEF00AA81bBCF694bC5c05A397F5E5658D5" - await bamm.swap(dec(1,18), dest, {from: whale}) + await bamm.swap(dec(105,18), priceWithFee.ethAmount, dest, {from: whale}) // check lusd balance - assert.equal(toBN(dec(6001, 18)).toString(), (await stabilityPool.getCompoundedLUSDDeposit(bamm.address)).toString()) + assert.equal(toBN(dec(6105, 18)).toString(), (await stabilityPool.getCompoundedLUSDDeposit(bamm.address)).toString()) // check eth balance - assert(await web3.eth.getBalance(dest), priceWithFee.ethAmount) + assert.equal(await web3.eth.getBalance(dest), priceWithFee.ethAmount) // check fees - assert(await web3.eth.getBalance(feePool), priceWithFee.feeEthAmount) + assert.equal(await web3.eth.getBalance(feePool), priceWithFee.feeEthAmount) }) it('test set params happy path', async () => { From 5120787ada09f8f7bd8e8af1afe78a35d247af9b Mon Sep 17 00:00:00 2001 From: yaron velner Date: Wed, 7 Jul 2021 17:58:36 +0300 Subject: [PATCH 20/90] more audit fixes --- packages/contracts/contracts/B.Protocol/BAMM.sol | 2 +- packages/contracts/contracts/B.Protocol/PriceFormula.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/contracts/B.Protocol/BAMM.sol b/packages/contracts/contracts/B.Protocol/BAMM.sol index 3fd4dda30..ee6ee65b6 100644 --- a/packages/contracts/contracts/B.Protocol/BAMM.sol +++ b/packages/contracts/contracts/B.Protocol/BAMM.sol @@ -188,7 +188,7 @@ contract BAMM is CropJoinAdapter, PriceFormula, Ownable { } // get ETH in return to LUSD - function swap(uint lusdAmount, uint minEthReturn, address payable dest) public payable returns(uint) { + function swap(uint lusdAmount, uint minEthReturn, address payable dest) public returns(uint) { (uint ethAmount, uint feeAmount) = getSwapEthAmount(lusdAmount); require(ethAmount >= minEthReturn, "swap: low return"); diff --git a/packages/contracts/contracts/B.Protocol/PriceFormula.sol b/packages/contracts/contracts/B.Protocol/PriceFormula.sol index 9bcafc6d8..a0545a318 100644 --- a/packages/contracts/contracts/B.Protocol/PriceFormula.sol +++ b/packages/contracts/contracts/B.Protocol/PriceFormula.sol @@ -44,7 +44,7 @@ contract PriceFormula { uint d = y.mul(2).add(b).sub(sum); y = n / d; - if(y <= yPrev.add(1) && yPrev.add(1) <= y) break; + if(y <= yPrev.add(1) && yPrev <= y.add(1)) break; } return yBalance.sub(y).sub(1); From 72b4611806275f1ac2ac70e97449a9f996c42c1b Mon Sep 17 00:00:00 2001 From: shmuel Date: Wed, 7 Jul 2021 18:46:43 +0300 Subject: [PATCH 21/90] temp --- package.json | 3 - packages/lib-ethers/abi/BAMM.json | 1137 +++++++++++++++++ .../deployments/default/goerli.json | 1 + .../lib-ethers/deployments/default/kovan.json | 1 + .../deployments/default/mainnet.json | 1 + .../deployments/default/rinkeby.json | 1 + .../deployments/default/ropsten.json | 1 + packages/lib-ethers/etc/lib-ethers.api.md | 4 + packages/lib-ethers/scripts/generate-types.ts | 2 + packages/lib-ethers/src/EthersLiquity.ts | 5 + .../src/PopulatableEthersLiquity.ts | 54 +- .../lib-ethers/src/ReadableEthersLiquity.ts | 78 +- packages/lib-ethers/src/contracts.ts | 4 + packages/lib-ethers/types/index.ts | 82 ++ packages/lib-ethers/utils/deploy.ts | 1 + 15 files changed, 1357 insertions(+), 18 deletions(-) create mode 100644 packages/lib-ethers/abi/BAMM.json diff --git a/package.json b/package.json index d2dcec6b7..d046dd94e 100644 --- a/package.json +++ b/package.json @@ -53,10 +53,7 @@ "prepare:lib-base": "yarn workspace @liquity/lib-base prepare", "prepare:lib-ethers": "yarn workspace @liquity/lib-ethers prepare", "prepare:lib-react": "yarn workspace @liquity/lib-react prepare", - "prepare:lib-subgraph": "yarn workspace @liquity/lib-subgraph prepare", "prepare:providers": "yarn workspace @liquity/providers prepare", - "prepare:subgraph": "yarn workspace @liquity/subgraph prepare", - "prepare:docs": "run-s docs", "rebuild": "run-s prepare build", "release": "run-s release:*", "release:delete-dev-deployments": "yarn workspace @liquity/lib-ethers delete-dev-deployments", diff --git a/packages/lib-ethers/abi/BAMM.json b/packages/lib-ethers/abi/BAMM.json new file mode 100644 index 000000000..4b3369420 --- /dev/null +++ b/packages/lib-ethers/abi/BAMM.json @@ -0,0 +1,1137 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_priceAggregator", + "type": "address" + }, + { + "internalType": "address payable", + "name": "_SP", + "type": "address" + }, + { + "internalType": "address", + "name": "_LUSD", + "type": "address" + }, + { + "internalType": "address", + "name": "_LQTY", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_maxDiscount", + "type": "uint256" + }, + { + "internalType": "address payable", + "name": "_feePool", + "type": "address" + }, + { + "internalType": "address", + "name": "_fronEndTag", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "val", + "type": "uint256" + } + ], + "name": "Exit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "Flee", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "val", + "type": "uint256" + } + ], + "name": "Join", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "A", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "ParamsSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "lusdAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "ethAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "RebalanceSwap", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "Tack", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "lusdAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "numShares", + "type": "uint256" + } + ], + "name": "UserDeposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "lusdAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "ethAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "numShares", + "type": "uint256" + } + ], + "name": "UserWithdraw", + "type": "event" + }, + { + "inputs": [], + "name": "A", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "LUSD", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_A", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_FEE", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MIN_A", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "PRECISION", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "SP", + "outputs": [ + { + "internalType": "contract StabilityPool", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "name": "add", + "outputs": [ + { + "internalType": "uint256", + "name": "z", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "bonus", + "outputs": [ + { + "internalType": "contract ERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "crops", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "dec", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "lusdAmount", + "type": "uint256" + } + ], + "name": "deposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "fee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "feePool", + "outputs": [ + { + "internalType": "address payable", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "fetchPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "frontEndTag", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "gem", + "outputs": [ + { + "internalType": "contract ERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "srcQty", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "getConversionRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "xQty", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "xBalance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "yBalance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "A", + "type": "uint256" + } + ], + "name": "getReturn", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "A", + "type": "uint256" + } + ], + "name": "getSumFixedPoint", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "lusdQty", + "type": "uint256" + } + ], + "name": "getSwapEthAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "ethAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "feeEthAmount", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "ilk", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isOwner", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxDiscount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "name": "mul", + "outputs": [ + { + "internalType": "uint256", + "name": "z", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nav", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "nps", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "priceAggregator", + "outputs": [ + { + "internalType": "contract AggregatorV3Interface", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "name": "rdiv", + "outputs": [ + { + "internalType": "uint256", + "name": "z", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "name": "rmul", + "outputs": [ + { + "internalType": "uint256", + "name": "z", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "name": "rmulup", + "outputs": [ + { + "internalType": "uint256", + "name": "z", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_A", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_fee", + "type": "uint256" + } + ], + "name": "setParams", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "share", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "stake", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "stock", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "name": "sub", + "outputs": [ + { + "internalType": "uint256", + "name": "z", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "lusdAmount", + "type": "uint256" + }, + { + "internalType": "address payable", + "name": "dest", + "type": "address" + } + ], + "name": "swap", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "total", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "srcAmount", + "type": "uint256" + }, + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + }, + { + "internalType": "address payable", + "name": "destAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "name": "trade", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "vat", + "outputs": [ + { + "internalType": "contract VatLike", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "name": "wdiv", + "outputs": [ + { + "internalType": "uint256", + "name": "z", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "name": "wdivup", + "outputs": [ + { + "internalType": "uint256", + "name": "z", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "numShares", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "name": "wmul", + "outputs": [ + { + "internalType": "uint256", + "name": "z", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] \ No newline at end of file diff --git a/packages/lib-ethers/deployments/default/goerli.json b/packages/lib-ethers/deployments/default/goerli.json index c55eb16a1..89b5ef378 100644 --- a/packages/lib-ethers/deployments/default/goerli.json +++ b/packages/lib-ethers/deployments/default/goerli.json @@ -22,6 +22,7 @@ "priceFeed": "0x7620B306164bc5d5491b99b8ACd515a06b15f29B", "sortedTroves": "0xBf7022bae995A8690A14e59284c48F7dF6b9F6F2", "stabilityPool": "0xC3809338e11f64B6a0b04c06AB3aC180bD781520", + "bamm": "0xaB875C981e1ee054b4C8A6F17DE8461a0dB55b80", "gasPool": "0x9440CD7990b122099A8eF59F74668206C660eED5", "unipool": "0xf195b1B4E8F74C91a4D3013626752eB108D7Ed9B", "lusdToken": "0xfBf329Bd38D57e0db8777915d4835341b97052A9", diff --git a/packages/lib-ethers/deployments/default/kovan.json b/packages/lib-ethers/deployments/default/kovan.json index 903df9714..b37e4d4d9 100644 --- a/packages/lib-ethers/deployments/default/kovan.json +++ b/packages/lib-ethers/deployments/default/kovan.json @@ -22,6 +22,7 @@ "priceFeed": "0xbA49275F8F890E7296F64b3e81F1Ada656030150", "sortedTroves": "0x91656701b33eca6425A239930FccAA842D0E2031", "stabilityPool": "0x04d630Bff6dea193Fd644dEcfC460db249854a02", + "bamm": "0x111CA91E821013F2Ab60652adeFB5115b9521CBF", "gasPool": "0xd97E194C0659F0d7b051EdF3E317BF4F7A675770", "unipool": "0xeed1782CaD8bad6eD2072C85e4c5821e67cCf1E1", "lusdToken": "0x0b02b94638daa719290b5214825dA625af08A02F", diff --git a/packages/lib-ethers/deployments/default/mainnet.json b/packages/lib-ethers/deployments/default/mainnet.json index 8cc61c129..4611f3787 100644 --- a/packages/lib-ethers/deployments/default/mainnet.json +++ b/packages/lib-ethers/deployments/default/mainnet.json @@ -22,6 +22,7 @@ "priceFeed": "0x4c517D4e2C851CA76d7eC94B805269Df0f2201De", "sortedTroves": "0x8FdD3fbFEb32b28fb73555518f8b361bCeA741A6", "stabilityPool": "0x66017D22b0f8556afDd19FC67041899Eb65a21bb", + "bamm": "0xaB875C981e1ee054b4C8A6F17DE8461a0dB55b80", "gasPool": "0x9555b042F969E561855e5F28cB1230819149A8d9", "unipool": "0xd37a77E71ddF3373a79BE2eBB76B6c4808bDF0d5", "lusdToken": "0x5f98805A4E8be255a32880FDeC7F6728C6568bA0", diff --git a/packages/lib-ethers/deployments/default/rinkeby.json b/packages/lib-ethers/deployments/default/rinkeby.json index d62dff24c..224a377e6 100644 --- a/packages/lib-ethers/deployments/default/rinkeby.json +++ b/packages/lib-ethers/deployments/default/rinkeby.json @@ -22,6 +22,7 @@ "priceFeed": "0x692Cd2D9Df7EFD91Ccd17F9eEFD72BAEE0584DB9", "sortedTroves": "0xac064890c6343F67450ba7aB97df6De38A8D7da8", "stabilityPool": "0xB8eb11f9eFF55378dfB692296C32DF020f5CC7fF", + "bamm": "0xaB875C981e1ee054b4C8A6F17DE8461a0dB55b80", "gasPool": "0xbbb26b40c1B32ba1F342DAbC65234516dd29BB44", "unipool": "0x866b252Acff4c45d6978032865aFb9923D11992A", "lusdToken": "0x9C5AE6852622ddE455B6Fca4C1551FC0352531a3", diff --git a/packages/lib-ethers/deployments/default/ropsten.json b/packages/lib-ethers/deployments/default/ropsten.json index 648d60126..a3b6510c7 100644 --- a/packages/lib-ethers/deployments/default/ropsten.json +++ b/packages/lib-ethers/deployments/default/ropsten.json @@ -22,6 +22,7 @@ "priceFeed": "0x7151f3828948B8824C677DA8daf7BD13014c1187", "sortedTroves": "0xA401f217DB7d84432C98272F637E9d450c6D16f2", "stabilityPool": "0x02dD2a33d0bBF40343CD09941F275978b1cd4ab9", + "bamm": "0xaB875C981e1ee054b4C8A6F17DE8461a0dB55b80", "gasPool": "0x8c2706b7bF86576F16Db7C099F5a62E7Ce8F0661", "unipool": "0x8C2C33247A691a98d05B60c2D7448687b6C56a86", "lusdToken": "0x99Fda92878c1d2f1e0971D1937C50CC578A33E3D", diff --git a/packages/lib-ethers/etc/lib-ethers.api.md b/packages/lib-ethers/etc/lib-ethers.api.md index a4a313dec..0ec038578 100644 --- a/packages/lib-ethers/etc/lib-ethers.api.md +++ b/packages/lib-ethers/etc/lib-ethers.api.md @@ -183,6 +183,8 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity getUniTokenAllowance(address?: string, overrides?: EthersCallOverrides): Promise; // (undocumented) getUniTokenBalance(address?: string, overrides?: EthersCallOverrides): Promise; + // (undocumented) + getWitdrawsSpShare(withdrawAmount: Decimalish): Promise; hasStore(): this is EthersLiquityWithStore; hasStore(store: "blockPolled"): this is EthersLiquityWithStore; // (undocumented) @@ -518,6 +520,8 @@ export class ReadableEthersLiquity implements ReadableLiquity { getUniTokenAllowance(address?: string, overrides?: EthersCallOverrides): Promise; // (undocumented) getUniTokenBalance(address?: string, overrides?: EthersCallOverrides): Promise; + // (undocumented) + getWitdrawsSpShare(withdrawAmount: Decimalish): Promise; hasStore(): this is ReadableEthersLiquityWithStore; hasStore(store: "blockPolled"): this is ReadableEthersLiquityWithStore; } diff --git a/packages/lib-ethers/scripts/generate-types.ts b/packages/lib-ethers/scripts/generate-types.ts index a51570ab3..fce9940ed 100644 --- a/packages/lib-ethers/scripts/generate-types.ts +++ b/packages/lib-ethers/scripts/generate-types.ts @@ -21,6 +21,7 @@ import PriceFeed from "../../contracts/artifacts/contracts/PriceFeed.sol/PriceFe import PriceFeedTestnet from "../../contracts/artifacts/contracts/TestContracts/PriceFeedTestnet.sol/PriceFeedTestnet.json"; import SortedTroves from "../../contracts/artifacts/contracts/SortedTroves.sol/SortedTroves.json"; import StabilityPool from "../../contracts/artifacts/contracts/StabilityPool.sol/StabilityPool.json"; +import BAMM from "../../contracts/artifacts/contracts/B.Protocol/BAMM.sol/BAMM.json"; import TroveManager from "../../contracts/artifacts/contracts/TroveManager.sol/TroveManager.json"; import Unipool from "../../contracts/artifacts/contracts/LPRewards/Unipool.sol/Unipool.json"; @@ -161,6 +162,7 @@ const contractArtifacts = [ PriceFeedTestnet, SortedTroves, StabilityPool, + BAMM, TroveManager, Unipool ]; diff --git a/packages/lib-ethers/src/EthersLiquity.ts b/packages/lib-ethers/src/EthersLiquity.ts index 909e4bc32..6a504c0d3 100644 --- a/packages/lib-ethers/src/EthersLiquity.ts +++ b/packages/lib-ethers/src/EthersLiquity.ts @@ -205,6 +205,11 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity return this._readable.getStabilityDeposit(address, overrides); } + /** {@inheritDoc @liquity/lib-base#ReadableLiquity.getWitdrawsSpShare} */ + getWitdrawsSpShare(withdrawAmount: Decimalish): Promise { + return this._readable.getWitdrawsSpShare(withdrawAmount); + } + /** {@inheritDoc @liquity/lib-base#ReadableLiquity.getRemainingStabilityPoolLQTYReward} */ getRemainingStabilityPoolLQTYReward(overrides?: EthersCallOverrides): Promise { return this._readable.getRemainingStabilityPoolLQTYReward(overrides); diff --git a/packages/lib-ethers/src/PopulatableEthersLiquity.ts b/packages/lib-ethers/src/PopulatableEthersLiquity.ts index d39bd6b79..bc1d39a20 100644 --- a/packages/lib-ethers/src/PopulatableEthersLiquity.ts +++ b/packages/lib-ethers/src/PopulatableEthersLiquity.ts @@ -1066,22 +1066,41 @@ export class PopulatableEthersLiquity ); } + // /** {@inheritDoc @liquity/lib-base#PopulatableLiquity.depositLUSDInStabilityPool} */ + // async depositLUSDInStabilityPool( + // amount: Decimalish, + // frontendTag?: string, + // overrides?: EthersTransactionOverrides + // ): Promise> { + // const { stabilityPool } = _getContracts(this._readable.connection); + // const depositLUSD = Decimal.from(amount); + + // return this._wrapStabilityDepositTopup( + // { depositLUSD }, + // await stabilityPool.estimateAndPopulate.provideToSP( + // { ...overrides }, + // addGasForLQTYIssuance, + // depositLUSD.hex, + // frontendTag ?? this._readable.connection.frontendTag ?? AddressZero + // ) + // ); + // } /** {@inheritDoc @liquity/lib-base#PopulatableLiquity.depositLUSDInStabilityPool} */ async depositLUSDInStabilityPool( amount: Decimalish, frontendTag?: string, overrides?: EthersTransactionOverrides ): Promise> { - const { stabilityPool } = _getContracts(this._readable.connection); + const { bamm } = _getContracts(this._readable.connection); const depositLUSD = Decimal.from(amount); - + return this._wrapStabilityDepositTopup( { depositLUSD }, - await stabilityPool.estimateAndPopulate.provideToSP( + await bamm.estimateAndPopulate.deposit( { ...overrides }, addGasForLQTYIssuance, - depositLUSD.hex, - frontendTag ?? this._readable.connection.frontendTag ?? AddressZero + depositLUSD.hex + // frontendTag ?? this._readable.connection.frontendTag ?? AddressZero ) ); } @@ -1091,17 +1110,34 @@ export class PopulatableEthersLiquity amount: Decimalish, overrides?: EthersTransactionOverrides ): Promise> { - const { stabilityPool } = _getContracts(this._readable.connection); - + const { bamm } = _getContracts(this._readable.connection); + // totalShare times witdrawLusd divide by totalLusd + const spShareToWithdraw = await this._readable.getWitdrawsSpShare(amount) return this._wrapStabilityDepositWithdrawal( - await stabilityPool.estimateAndPopulate.withdrawFromSP( + await bamm.estimateAndPopulate.withdraw( { ...overrides }, addGasForLQTYIssuance, - Decimal.from(amount).hex + Decimal.from(spShareToWithdraw).hex ) ); } + // /** {@inheritDoc @liquity/lib-base#PopulatableLiquity.withdrawLUSDFromStabilityPool} */ + // async withdrawLUSDFromStabilityPool( + // amount: Decimalish, + // overrides?: EthersTransactionOverrides + // ): Promise> { + // const { stabilityPool } = _getContracts(this._readable.connection); + + // return this._wrapStabilityDepositWithdrawal( + // await stabilityPool.estimateAndPopulate.withdrawFromSP( + // { ...overrides }, + // addGasForLQTYIssuance, + // Decimal.from(amount).hex + // ) + // ); + // } + /** {@inheritDoc @liquity/lib-base#PopulatableLiquity.withdrawGainsFromStabilityPool} */ async withdrawGainsFromStabilityPool( overrides?: EthersTransactionOverrides diff --git a/packages/lib-ethers/src/ReadableEthersLiquity.ts b/packages/lib-ethers/src/ReadableEthersLiquity.ts index 4d1a927f2..00c3c1d41 100644 --- a/packages/lib-ethers/src/ReadableEthersLiquity.ts +++ b/packages/lib-ethers/src/ReadableEthersLiquity.ts @@ -2,6 +2,7 @@ import { BlockTag } from "@ethersproject/abstract-provider"; import { Decimal, + Decimalish, Fees, FrontendStatus, LiquityStore, @@ -247,35 +248,96 @@ export class ReadableEthersLiquity implements ReadableLiquity { return activePool.add(defaultPool); } + async getWitdrawsSpShare( + withdrawAmount: Decimalish, // todo should be Decimalish + ): Promise { + const address = _requireAddress(this.connection); + const { stabilityPool, bamm } = _getContracts(this.connection); + + console.log(withdrawAmount) + + const [ + currentBammLUSD, + total, + stake + ] = await Promise.all([ + stabilityPool.getCompoundedLUSDDeposit(bamm.address), + bamm.total(), + bamm.stake(address) + ]); + console.log({stake}) + // totalShare times witdrawLusd divide by totalLusd + const spShare = decimalify(total).mul(Decimal.from(withdrawAmount)).div(decimalify(currentBammLUSD)).toString() + + return spShare + } + /** {@inheritDoc @liquity/lib-base#ReadableLiquity.getStabilityDeposit} */ async getStabilityDeposit( address?: string, overrides?: EthersCallOverrides ): Promise { address ??= _requireAddress(this.connection); - const { stabilityPool } = _getContracts(this.connection); + const { stabilityPool, bamm } = _getContracts(this.connection); const [ { frontEndTag, initialValue }, - currentLUSD, + currentBammLUSD, collateralGain, - lqtyReward + lqtyReward, + total, + stake ] = await Promise.all([ stabilityPool.deposits(address, { ...overrides }), - stabilityPool.getCompoundedLUSDDeposit(address, { ...overrides }), + // todo bamm.add, bamm.total, bamm.stake + stabilityPool.getCompoundedLUSDDeposit(bamm.address, { ...overrides }), stabilityPool.getDepositorETHGain(address, { ...overrides }), - stabilityPool.getDepositorLQTYGain(address, { ...overrides }) + stabilityPool.getDepositorLQTYGain(address, { ...overrides }), + bamm.total({ ...overrides }), + bamm.stake(address, { ...overrides}) ]); + // stake times lusd dived by total + const currentLUSD = decimalify(stake).mul(decimalify(currentBammLUSD)).div(decimalify(total)) + return new StabilityDeposit( decimalify(initialValue), - decimalify(currentLUSD), + currentLUSD, decimalify(collateralGain), decimalify(lqtyReward), frontEndTag ); } + // /** {@inheritDoc @liquity/lib-base#ReadableLiquity.getStabilityDeposit} */ + // async getStabilityDeposit( + // address?: string, + // overrides?: EthersCallOverrides + // ): Promise { + // address ??= _requireAddress(this.connection); + // const { stabilityPool } = _getContracts(this.connection); + + // const [ + // { frontEndTag, initialValue }, + // currentLUSD, + // collateralGain, + // lqtyReward + // ] = await Promise.all([ + // stabilityPool.deposits(address, { ...overrides }), + // stabilityPool.getCompoundedLUSDDeposit(address, { ...overrides }), + // stabilityPool.getDepositorETHGain(address, { ...overrides }), + // stabilityPool.getDepositorLQTYGain(address, { ...overrides }) + // ]); + + // return new StabilityDeposit( + // decimalify(initialValue), + // decimalify(currentLUSD), + // decimalify(collateralGain), + // decimalify(lqtyReward), + // frontEndTag + // ); + // } + /** {@inheritDoc @liquity/lib-base#ReadableLiquity.getRemainingStabilityPoolLQTYReward} */ async getRemainingStabilityPoolLQTYReward(overrides?: EthersCallOverrides): Promise { const { communityIssuance } = _getContracts(this.connection); @@ -611,6 +673,10 @@ class _BlockPolledReadableEthersLiquity return this._blockHit(overrides) ? this.store.state.total : this._readable.getTotal(overrides); } + async getWitdrawsSpShare(withdrawAmount: Decimalish): Promise { + return this._readable.getWitdrawsSpShare(withdrawAmount) + } + async getStabilityDeposit( address?: string, overrides?: EthersCallOverrides diff --git a/packages/lib-ethers/src/contracts.ts b/packages/lib-ethers/src/contracts.ts index 25a23685b..e2f5c796e 100644 --- a/packages/lib-ethers/src/contracts.ts +++ b/packages/lib-ethers/src/contracts.ts @@ -28,6 +28,7 @@ import priceFeedAbi from "../abi/PriceFeed.json"; import priceFeedTestnetAbi from "../abi/PriceFeedTestnet.json"; import sortedTrovesAbi from "../abi/SortedTroves.json"; import stabilityPoolAbi from "../abi/StabilityPool.json"; +import BAMMAbi from "../abi/BAMM.json"; import gasPoolAbi from "../abi/GasPool.json"; import unipoolAbi from "../abi/Unipool.json"; import iERC20Abi from "../abi/IERC20.json"; @@ -50,6 +51,7 @@ import { PriceFeedTestnet, SortedTroves, StabilityPool, + BAMM, GasPool, Unipool, ERC20Mock, @@ -180,6 +182,7 @@ export interface _LiquityContracts { priceFeed: PriceFeed | PriceFeedTestnet; sortedTroves: SortedTroves; stabilityPool: StabilityPool; + bamm: BAMM; gasPool: GasPool; unipool: Unipool; uniToken: IERC20 | ERC20Mock; @@ -216,6 +219,7 @@ const getAbi = (priceFeedIsTestnet: boolean, uniTokenIsMock: boolean): LiquityCo priceFeed: priceFeedIsTestnet ? priceFeedTestnetAbi : priceFeedAbi, sortedTroves: sortedTrovesAbi, stabilityPool: stabilityPoolAbi, + bamm: BAMMAbi, gasPool: gasPoolAbi, collSurplusPool: collSurplusPoolAbi, unipool: unipoolAbi, diff --git a/packages/lib-ethers/types/index.ts b/packages/lib-ethers/types/index.ts index bb85ff4c4..9161c01a2 100644 --- a/packages/lib-ethers/types/index.ts +++ b/packages/lib-ethers/types/index.ts @@ -750,6 +750,88 @@ export interface StabilityPool extractEvents(logs: Log[], name: "UserDepositChanged"): _TypedLogDescription<{ _depositor: string; _newDeposit: BigNumber }>[]; } +interface BAMMCalls { + A(_overrides?: CallOverrides): Promise; + LUSD(_overrides?: CallOverrides): Promise; + MAX_A(_overrides?: CallOverrides): Promise; + MAX_FEE(_overrides?: CallOverrides): Promise; + MIN_A(_overrides?: CallOverrides): Promise; + PRECISION(_overrides?: CallOverrides): Promise; + SP(_overrides?: CallOverrides): Promise; + add(x: BigNumberish, y: BigNumberish, _overrides?: CallOverrides): Promise; + balanceOf(owner: string, _overrides?: CallOverrides): Promise; + bonus(_overrides?: CallOverrides): Promise; + crops(arg0: string, _overrides?: CallOverrides): Promise; + dec(_overrides?: CallOverrides): Promise; + decimals(_overrides?: CallOverrides): Promise; + fee(_overrides?: CallOverrides): Promise; + feePool(_overrides?: CallOverrides): Promise; + fetchPrice(_overrides?: CallOverrides): Promise; + frontEndTag(_overrides?: CallOverrides): Promise; + gem(_overrides?: CallOverrides): Promise; + getConversionRate(arg0: string, arg1: string, srcQty: BigNumberish, arg3: BigNumberish, _overrides?: CallOverrides): Promise; + getReturn(xQty: BigNumberish, xBalance: BigNumberish, yBalance: BigNumberish, A: BigNumberish, _overrides?: CallOverrides): Promise; + getSumFixedPoint(x: BigNumberish, y: BigNumberish, A: BigNumberish, _overrides?: CallOverrides): Promise; + getSwapEthAmount(lusdQty: BigNumberish, _overrides?: CallOverrides): Promise<{ ethAmount: BigNumber; feeEthAmount: BigNumber }>; + ilk(_overrides?: CallOverrides): Promise; + isOwner(_overrides?: CallOverrides): Promise; + maxDiscount(_overrides?: CallOverrides): Promise; + mul(x: BigNumberish, y: BigNumberish, _overrides?: CallOverrides): Promise; + name(_overrides?: CallOverrides): Promise; + owner(_overrides?: CallOverrides): Promise; + priceAggregator(_overrides?: CallOverrides): Promise; + rdiv(x: BigNumberish, y: BigNumberish, _overrides?: CallOverrides): Promise; + rmul(x: BigNumberish, y: BigNumberish, _overrides?: CallOverrides): Promise; + rmulup(x: BigNumberish, y: BigNumberish, _overrides?: CallOverrides): Promise; + share(_overrides?: CallOverrides): Promise; + stake(arg0: string, _overrides?: CallOverrides): Promise; + stock(_overrides?: CallOverrides): Promise; + sub(x: BigNumberish, y: BigNumberish, _overrides?: CallOverrides): Promise; + symbol(_overrides?: CallOverrides): Promise; + total(_overrides?: CallOverrides): Promise; + totalSupply(_overrides?: CallOverrides): Promise; + vat(_overrides?: CallOverrides): Promise; + wdiv(x: BigNumberish, y: BigNumberish, _overrides?: CallOverrides): Promise; + wdivup(x: BigNumberish, y: BigNumberish, _overrides?: CallOverrides): Promise; + wmul(x: BigNumberish, y: BigNumberish, _overrides?: CallOverrides): Promise; +} + +interface BAMMTransactions { + deposit(lusdAmount: BigNumberish, _overrides?: Overrides): Promise; + nav(_overrides?: Overrides): Promise; + nps(_overrides?: Overrides): Promise; + setParams(_A: BigNumberish, _fee: BigNumberish, _overrides?: Overrides): Promise; + swap(lusdAmount: BigNumberish, dest: string, _overrides?: PayableOverrides): Promise; + trade(arg0: string, srcAmount: BigNumberish, arg2: string, destAddress: string, arg4: BigNumberish, arg5: boolean, _overrides?: PayableOverrides): Promise; + withdraw(numShares: BigNumberish, _overrides?: Overrides): Promise; +} + +export interface BAMM + extends _TypedLiquityContract { + readonly filters: { + Exit(val?: null): EventFilter; + Flee(): EventFilter; + Join(val?: null): EventFilter; + OwnershipTransferred(previousOwner?: string | null, newOwner?: string | null): EventFilter; + ParamsSet(A?: null, fee?: null): EventFilter; + RebalanceSwap(user?: string | null, lusdAmount?: null, ethAmount?: null, timestamp?: null): EventFilter; + Tack(src?: string | null, dst?: string | null, wad?: null): EventFilter; + Transfer(_from?: string | null, _to?: string | null, _value?: null): EventFilter; + UserDeposit(user?: string | null, lusdAmount?: null, numShares?: null): EventFilter; + UserWithdraw(user?: string | null, lusdAmount?: null, ethAmount?: null, numShares?: null): EventFilter; + }; + extractEvents(logs: Log[], name: "Exit"): _TypedLogDescription<{ val: BigNumber }>[]; + extractEvents(logs: Log[], name: "Flee"): _TypedLogDescription<{ }>[]; + extractEvents(logs: Log[], name: "Join"): _TypedLogDescription<{ val: BigNumber }>[]; + extractEvents(logs: Log[], name: "OwnershipTransferred"): _TypedLogDescription<{ previousOwner: string; newOwner: string }>[]; + extractEvents(logs: Log[], name: "ParamsSet"): _TypedLogDescription<{ A: BigNumber; fee: BigNumber }>[]; + extractEvents(logs: Log[], name: "RebalanceSwap"): _TypedLogDescription<{ user: string; lusdAmount: BigNumber; ethAmount: BigNumber; timestamp: BigNumber }>[]; + extractEvents(logs: Log[], name: "Tack"): _TypedLogDescription<{ src: string; dst: string; wad: BigNumber }>[]; + extractEvents(logs: Log[], name: "Transfer"): _TypedLogDescription<{ _from: string; _to: string; _value: BigNumber }>[]; + extractEvents(logs: Log[], name: "UserDeposit"): _TypedLogDescription<{ user: string; lusdAmount: BigNumber; numShares: BigNumber }>[]; + extractEvents(logs: Log[], name: "UserWithdraw"): _TypedLogDescription<{ user: string; lusdAmount: BigNumber; ethAmount: BigNumber; numShares: BigNumber }>[]; +} + interface TroveManagerCalls { BETA(_overrides?: CallOverrides): Promise; BOOTSTRAP_PERIOD(_overrides?: CallOverrides): Promise; diff --git a/packages/lib-ethers/utils/deploy.ts b/packages/lib-ethers/utils/deploy.ts index 74d0708c6..2523b77e1 100644 --- a/packages/lib-ethers/utils/deploy.ts +++ b/packages/lib-ethers/utils/deploy.ts @@ -170,6 +170,7 @@ const connectContracts = async ( priceFeed, sortedTroves, stabilityPool, + bamm, gasPool, unipool, uniToken From 009325634e52e43d63e338ff462f792b07274af9 Mon Sep 17 00:00:00 2001 From: yaron velner Date: Wed, 7 Jul 2021 20:26:22 +0300 Subject: [PATCH 22/90] papers --- papers/B.Protocol/TODO.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 papers/B.Protocol/TODO.txt diff --git a/papers/B.Protocol/TODO.txt b/papers/B.Protocol/TODO.txt new file mode 100644 index 000000000..e69de29bb From 4f9a9d0337c3e72397a2f30347688f1042fe9d4f Mon Sep 17 00:00:00 2001 From: Kurt Barry Date: Wed, 7 Jul 2021 15:17:23 -0400 Subject: [PATCH 23/90] add smart contract assessment by Fixed Point Solutions --- .../FPS_B.AMM_Liquity_Assessment_FINAL.pdf | Bin 0 -> 131186 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 papers/B.Protocol/FPS_B.AMM_Liquity_Assessment_FINAL.pdf diff --git a/papers/B.Protocol/FPS_B.AMM_Liquity_Assessment_FINAL.pdf b/papers/B.Protocol/FPS_B.AMM_Liquity_Assessment_FINAL.pdf new file mode 100644 index 0000000000000000000000000000000000000000..45ef69dcdcca309ef292fee59612db4603345445 GIT binary patch literal 131186 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π$;@2OV(3|9`;*ds|7@u2d}wz%BuD=&YUuJ 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 Q0 Date: Wed, 7 Jul 2021 22:22:41 +0300 Subject: [PATCH 24/90] Delete TODO.txt --- papers/B.Protocol/TODO.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 papers/B.Protocol/TODO.txt diff --git a/papers/B.Protocol/TODO.txt b/papers/B.Protocol/TODO.txt deleted file mode 100644 index e69de29bb..000000000 From b1e9a60ce4940869142059732097db8d308879ff Mon Sep 17 00:00:00 2001 From: yaron velner Date: Thu, 8 Jul 2021 14:56:17 +0300 Subject: [PATCH 25/90] ownable - allow transfer ownership --- packages/contracts/contracts/Dependencies/Ownable.sol | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/contracts/contracts/Dependencies/Ownable.sol b/packages/contracts/contracts/Dependencies/Ownable.sol index d1f4826e6..39fcb3fce 100644 --- a/packages/contracts/contracts/Dependencies/Ownable.sol +++ b/packages/contracts/contracts/Dependencies/Ownable.sol @@ -49,6 +49,17 @@ contract Ownable { return msg.sender == _owner; } + function transferOwnership(address newOwner) public virtual onlyOwner { + require(newOwner != address(0), "Ownable: new owner is the zero address"); + _setOwner(newOwner); + } + + function _setOwner(address newOwner) private { + address oldOwner = _owner; + _owner = newOwner; + emit OwnershipTransferred(oldOwner, newOwner); + } + /** * @dev Leaves the contract without owner. It will not be possible to call * `onlyOwner` functions anymore. From ca76864b507c254f41cbf86704a245355d50126b Mon Sep 17 00:00:00 2001 From: shmuel Date: Sun, 11 Jul 2021 11:48:38 +0300 Subject: [PATCH 26/90] stabilityDeposit.poolShare --- .../components/Stability/ActiveDeposit.tsx | 2 +- packages/lib-base/etc/lib-base.api.md | 3 ++- packages/lib-base/src/StabilityDeposit.ts | 8 ++++--- .../lib-ethers/src/BlockPolledLiquityStore.ts | 1 + .../src/PopulatableEthersLiquity.ts | 4 ++-- .../lib-ethers/src/ReadableEthersLiquity.ts | 22 ++++++++++++++----- 6 files changed, 27 insertions(+), 13 deletions(-) diff --git a/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx b/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx index 77327210d..fdaa6cf64 100644 --- a/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx +++ b/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx @@ -26,7 +26,7 @@ export const ActiveDeposit: React.FC = () => { const { dispatchEvent } = useStabilityView(); const { stabilityDeposit, trove, lusdInStabilityPool } = useLiquitySelector(selector); - const poolShare = stabilityDeposit.currentLUSD.mulDiv(100, lusdInStabilityPool); + const poolShare = stabilityDeposit.poolShare const handleAdjustDeposit = useCallback(() => { dispatchEvent("ADJUST_DEPOSIT_PRESSED"); diff --git a/packages/lib-base/etc/lib-base.api.md b/packages/lib-base/etc/lib-base.api.md index 57ed95e5d..74593f2f4 100644 --- a/packages/lib-base/etc/lib-base.api.md +++ b/packages/lib-base/etc/lib-base.api.md @@ -591,7 +591,7 @@ export interface SentLiquityTransaction | undefined): Decimal; readonly collateralGain: Decimal; readonly currentLUSD: Decimal; @@ -601,6 +601,7 @@ export class StabilityDeposit { // (undocumented) get isEmpty(): boolean; readonly lqtyReward: Decimal; + readonly poolShare: Decimal; // @internal (undocumented) toString(): string; whatChanged(thatLUSD: Decimalish): StabilityDepositChange | undefined; diff --git a/packages/lib-base/src/StabilityDeposit.ts b/packages/lib-base/src/StabilityDeposit.ts index 179772c6c..0460861a4 100644 --- a/packages/lib-base/src/StabilityDeposit.ts +++ b/packages/lib-base/src/StabilityDeposit.ts @@ -15,6 +15,9 @@ export type StabilityDepositChange = * @public */ export class StabilityDeposit { + /** pool share of user in the BAMM wich has a share in the stability pool */ + readonly poolShare: Decimal; + /** Amount of LUSD in the Stability Deposit at the time of the last direct modification. */ readonly initialLUSD: Decimal; @@ -38,21 +41,20 @@ export class StabilityDeposit { /** @internal */ constructor( + poolShare: Decimal, initialLUSD: Decimal, currentLUSD: Decimal, collateralGain: Decimal, lqtyReward: Decimal, frontendTag: string ) { + this.poolShare = poolShare; this.initialLUSD = initialLUSD; this.currentLUSD = currentLUSD; this.collateralGain = collateralGain; this.lqtyReward = lqtyReward; this.frontendTag = frontendTag; - if (this.currentLUSD.gt(this.initialLUSD)) { - throw new Error("currentLUSD can't be greater than initialLUSD"); - } } get isEmpty(): boolean { diff --git a/packages/lib-ethers/src/BlockPolledLiquityStore.ts b/packages/lib-ethers/src/BlockPolledLiquityStore.ts index 33aa03cd3..2e74f74e3 100644 --- a/packages/lib-ethers/src/BlockPolledLiquityStore.ts +++ b/packages/lib-ethers/src/BlockPolledLiquityStore.ts @@ -155,6 +155,7 @@ export class BlockPolledLiquityStore extends LiquityStore> { - const { stabilityPool } = _getContracts(this._readable.connection); + const { bamm } = _getContracts(this._readable.connection); return this._wrapStabilityPoolGainsWithdrawal( - await stabilityPool.estimateAndPopulate.withdrawFromSP( + await bamm.estimateAndPopulate.withdraw( { ...overrides }, addGasForLQTYIssuance, Decimal.ZERO.hex diff --git a/packages/lib-ethers/src/ReadableEthersLiquity.ts b/packages/lib-ethers/src/ReadableEthersLiquity.ts index 00c3c1d41..ce46c9d2f 100644 --- a/packages/lib-ethers/src/ReadableEthersLiquity.ts +++ b/packages/lib-ethers/src/ReadableEthersLiquity.ts @@ -257,9 +257,9 @@ export class ReadableEthersLiquity implements ReadableLiquity { console.log(withdrawAmount) const [ - currentBammLUSD, - total, - stake + currentBammLUSD, + total, // total amount of shares in the bamm + stake // users share in the bamm ] = await Promise.all([ stabilityPool.getCompoundedLUSDDeposit(bamm.address), bamm.total(), @@ -272,6 +272,8 @@ export class ReadableEthersLiquity implements ReadableLiquity { return spShare } + // bamm share in SP times stake div by total + /** {@inheritDoc @liquity/lib-base#ReadableLiquity.getStabilityDeposit} */ async getStabilityDeposit( address?: string, @@ -286,7 +288,8 @@ export class ReadableEthersLiquity implements ReadableLiquity { collateralGain, lqtyReward, total, - stake + stake, + totalLusdInSp, ] = await Promise.all([ stabilityPool.deposits(address, { ...overrides }), // todo bamm.add, bamm.total, bamm.stake @@ -294,13 +297,20 @@ export class ReadableEthersLiquity implements ReadableLiquity { stabilityPool.getDepositorETHGain(address, { ...overrides }), stabilityPool.getDepositorLQTYGain(address, { ...overrides }), bamm.total({ ...overrides }), - bamm.stake(address, { ...overrides}) + bamm.stake(address, { ...overrides}), + this.getLUSDInStabilityPool({...overrides}) ]); - // stake times lusd dived by total + // stake times lusd divided by total const currentLUSD = decimalify(stake).mul(decimalify(currentBammLUSD)).div(decimalify(total)) + + // stabilityDeposit.currentLUSD.mulDiv(100, lusdInStabilityPool); + const bammShare = decimalify(currentBammLUSD).mulDiv(100, totalLusdInSp) + // bamm share in SP times stake div by total + const poolShare = bammShare.mul(decimalify(stake)).div(decimalify(total)) return new StabilityDeposit( + poolShare, decimalify(initialValue), currentLUSD, decimalify(collateralGain), From 60bb75adf7538050d3c37eb49a362ebe4d49eb2f Mon Sep 17 00:00:00 2001 From: shmuel Date: Sun, 11 Jul 2021 11:48:54 +0300 Subject: [PATCH 27/90] cleanUp --- packages/lib-ethers/src/ReadableEthersLiquity.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/lib-ethers/src/ReadableEthersLiquity.ts b/packages/lib-ethers/src/ReadableEthersLiquity.ts index ce46c9d2f..ff18bfd83 100644 --- a/packages/lib-ethers/src/ReadableEthersLiquity.ts +++ b/packages/lib-ethers/src/ReadableEthersLiquity.ts @@ -272,8 +272,6 @@ export class ReadableEthersLiquity implements ReadableLiquity { return spShare } - // bamm share in SP times stake div by total - /** {@inheritDoc @liquity/lib-base#ReadableLiquity.getStabilityDeposit} */ async getStabilityDeposit( address?: string, @@ -303,7 +301,6 @@ export class ReadableEthersLiquity implements ReadableLiquity { // stake times lusd divided by total const currentLUSD = decimalify(stake).mul(decimalify(currentBammLUSD)).div(decimalify(total)) - // stabilityDeposit.currentLUSD.mulDiv(100, lusdInStabilityPool); const bammShare = decimalify(currentBammLUSD).mulDiv(100, totalLusdInSp) // bamm share in SP times stake div by total From 92f716f5986b224f00882fe8b869c44df44eeb80 Mon Sep 17 00:00:00 2001 From: shmuel Date: Sun, 11 Jul 2021 17:54:07 +0300 Subject: [PATCH 28/90] LQTY reward --- .../lib-ethers/src/ReadableEthersLiquity.ts | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/packages/lib-ethers/src/ReadableEthersLiquity.ts b/packages/lib-ethers/src/ReadableEthersLiquity.ts index ff18bfd83..4ecac1af0 100644 --- a/packages/lib-ethers/src/ReadableEthersLiquity.ts +++ b/packages/lib-ethers/src/ReadableEthersLiquity.ts @@ -277,41 +277,60 @@ export class ReadableEthersLiquity implements ReadableLiquity { address?: string, overrides?: EthersCallOverrides ): Promise { + const RAY = Decimal.from(10).pow(27) + const _1e18 = Decimal.from(10).pow(18) address ??= _requireAddress(this.connection); - const { stabilityPool, bamm } = _getContracts(this.connection); - + const { stabilityPool, bamm, lqtyToken } = _getContracts(this.connection); + const bammLqtyBalancePromise = lqtyToken.balanceOf(bamm.address, { ...overrides}) + const [ { frontEndTag, initialValue }, currentBammLUSD, collateralGain, - lqtyReward, + bammPendingLqtyReward, total, stake, totalLusdInSp, + crops, + share, + stock, ] = await Promise.all([ stabilityPool.deposits(address, { ...overrides }), - // todo bamm.add, bamm.total, bamm.stake - stabilityPool.getCompoundedLUSDDeposit(bamm.address, { ...overrides }), + stabilityPool.getCompoundedLUSDDeposit(bamm.address, { ...overrides }).then(decimalify), stabilityPool.getDepositorETHGain(address, { ...overrides }), - stabilityPool.getDepositorLQTYGain(address, { ...overrides }), - bamm.total({ ...overrides }), - bamm.stake(address, { ...overrides}), - this.getLUSDInStabilityPool({...overrides}) + stabilityPool.getDepositorLQTYGain(bamm.address, { ...overrides }).then(decimalify), + bamm.total({ ...overrides }).then(decimalify), + bamm.stake(address, { ...overrides}).then(decimalify), + stabilityPool.getTotalLUSDDeposits({ ...overrides }).then(decimalify), + bamm.crops(address, { ...overrides }).then(decimalify), + bamm.share({ ...overrides }).then(decimalify), + bamm.stock({ ...overrides}).then(decimalify), ]); + const bammLqtyBalance = await bammLqtyBalancePromise.then(decimalify) // stake times lusd divided by total - const currentLUSD = decimalify(stake).mul(decimalify(currentBammLUSD)).div(decimalify(total)) + const currentLUSD = stake.mul(currentBammLUSD).div(total) // stabilityDeposit.currentLUSD.mulDiv(100, lusdInStabilityPool); - const bammShare = decimalify(currentBammLUSD).mulDiv(100, totalLusdInSp) + const bammShare = currentBammLUSD.mul(100).div(totalLusdInSp) // bamm share in SP times stake div by total - const poolShare = bammShare.mul(decimalify(stake)).div(decimalify(total)) - + const poolShare = bammShare.mul(stake).div(total) + + // balance + pending - stock + let lqtyReward = Decimal.from(0) + if(total.gt(Decimal.from(0))){ + const crop = bammLqtyBalance.add(bammPendingLqtyReward).sub(stock); + const updatedShare = share.add(crop.mul(RAY).mul(_1e18).div(total)) + const updatedCrops = stake.mul(updatedShare).mul(_1e18).div(RAY) + if(updatedCrops.gt(crops)){ + lqtyReward = updatedCrops.sub(crops) + } + } return new StabilityDeposit( poolShare, decimalify(initialValue), currentLUSD, decimalify(collateralGain), - decimalify(lqtyReward), + lqtyReward, frontEndTag ); } From 1626072ff137d566dc2a12038744f8d739a7a60b Mon Sep 17 00:00:00 2001 From: shmuel Date: Mon, 12 Jul 2021 11:25:43 +0300 Subject: [PATCH 29/90] Changing liquidation gain to BAMM ETH --- .../components/Stability/ActiveDeposit.tsx | 2 +- .../lib-ethers/src/ReadableEthersLiquity.ts | 26 ++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx b/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx index fdaa6cf64..77afbb249 100644 --- a/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx +++ b/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx @@ -75,7 +75,7 @@ export const ActiveDeposit: React.FC = () => { /> Date: Mon, 12 Jul 2021 16:40:56 +0300 Subject: [PATCH 30/90] working state calculatin LQTY reward using BigNumber --- packages/lib-base/etc/lib-base.api.md | 6 +++ packages/lib-base/src/Decimal.ts | 14 ++++++ .../lib-ethers/src/ReadableEthersLiquity.ts | 47 ++++++++++--------- 3 files changed, 44 insertions(+), 23 deletions(-) diff --git a/packages/lib-base/etc/lib-base.api.md b/packages/lib-base/etc/lib-base.api.md index 74593f2f4..f0f32c44a 100644 --- a/packages/lib-base/etc/lib-base.api.md +++ b/packages/lib-base/etc/lib-base.api.md @@ -4,6 +4,8 @@ ```ts +import { BigNumber } from '@ethersproject/bignumber'; + // @internal (undocumented) export class _CachedReadableLiquity implements _ReadableLiquityWithExtraParams { constructor(readable: _ReadableLiquityWithExtraParams, cache: _LiquityReadCache); @@ -102,6 +104,8 @@ export class Decimal { // (undocumented) static from(decimalish: Decimalish): Decimal; // (undocumented) + static fromBigNumber(bigNumber: BigNumber): Decimal; + // (undocumented) static fromBigNumberString(bigNumberString: string): Decimal; // (undocumented) gt(that: Decimalish): boolean; @@ -138,6 +142,8 @@ export class Decimal { // (undocumented) prettify(precision?: number): string; // (undocumented) + rawDivision(divider: Decimalish): Decimal; + // (undocumented) shorten(): string; // (undocumented) sub(subtrahend: Decimalish): Decimal; diff --git a/packages/lib-base/src/Decimal.ts b/packages/lib-base/src/Decimal.ts index 12d117398..586c8fe25 100644 --- a/packages/lib-base/src/Decimal.ts +++ b/packages/lib-base/src/Decimal.ts @@ -62,6 +62,10 @@ export class Decimal { return new Decimal(BigNumber.from(bigNumberString)); } + static fromBigNumber(bigNumber: BigNumber): Decimal { + return new Decimal(bigNumber); + } + private static _fromString(representation: string): Decimal { if (!representation || !representation.match(stringRepresentationFormat)) { throw new Error(`bad decimal format: "${representation}"`); @@ -210,6 +214,16 @@ export class Decimal { return new Decimal(this._bigNumber.mul(DIGITS).div(divider._bigNumber)); } + rawDivision(divider: Decimalish): Decimal { + divider = Decimal.from(divider); + + if (divider.isZero) { + return Decimal.INFINITY; + } + + return new Decimal(this._bigNumber.div(divider._bigNumber)); + } + /** @internal */ _divCeil(divider: Decimalish): Decimal { divider = Decimal.from(divider); diff --git a/packages/lib-ethers/src/ReadableEthersLiquity.ts b/packages/lib-ethers/src/ReadableEthersLiquity.ts index 14280443f..b84027447 100644 --- a/packages/lib-ethers/src/ReadableEthersLiquity.ts +++ b/packages/lib-ethers/src/ReadableEthersLiquity.ts @@ -1,4 +1,5 @@ import { BlockTag } from "@ethersproject/abstract-provider"; +import { BigNumber } from "@ethersproject/bignumber"; import { Decimal, @@ -277,8 +278,8 @@ export class ReadableEthersLiquity implements ReadableLiquity { address?: string, overrides?: EthersCallOverrides ): Promise { - const RAY = Decimal.from(10).pow(27) - const _1e18 = Decimal.from(10).pow(18) + const RAY = BigNumber.from(10).pow(27) + const _1e18 = BigNumber.from(10).pow(18) address ??= _requireAddress(this.connection); const { stabilityPool, bamm, lqtyToken } = _getContracts(this.connection); const bammLqtyBalancePromise = lqtyToken.balanceOf(bamm.address, { ...overrides}) @@ -297,34 +298,34 @@ export class ReadableEthersLiquity implements ReadableLiquity { stock, ] = await Promise.all([ stabilityPool.deposits(address, { ...overrides }), - stabilityPool.getCompoundedLUSDDeposit(bamm.address, { ...overrides }).then(decimalify), - stabilityPool.getDepositorETHGain(bamm.address, { ...overrides }).then(decimalify), - stabilityPool.getDepositorLQTYGain(bamm.address, { ...overrides }).then(decimalify), - bamm.total({ ...overrides }).then(decimalify), - bamm.stake(address, { ...overrides}).then(decimalify), - stabilityPool.getTotalLUSDDeposits({ ...overrides }).then(decimalify), - bamm.crops(address, { ...overrides }).then(decimalify), - bamm.share({ ...overrides }).then(decimalify), - bamm.stock({ ...overrides}).then(decimalify), + stabilityPool.getCompoundedLUSDDeposit(bamm.address, { ...overrides }), + stabilityPool.getDepositorETHGain(bamm.address, { ...overrides }), + stabilityPool.getDepositorLQTYGain(bamm.address, { ...overrides }), + bamm.total({ ...overrides }), + bamm.stake(address, { ...overrides}), + stabilityPool.getTotalLUSDDeposits({ ...overrides }), + bamm.crops(address, { ...overrides }), + bamm.share({ ...overrides }), + bamm.stock({ ...overrides}), ]); - const bammLqtyBalance = await bammLqtyBalancePromise.then(decimalify) + const bammLqtyBalance = await bammLqtyBalancePromise // stake times lusd divided by total const currentLUSD = stake.mul(currentBammLUSD).div(total) // stabilityDeposit.currentLUSD.mulDiv(100, lusdInStabilityPool); - const bammShare = currentBammLUSD.mul(100).div(totalLusdInSp) + const bammShare = Decimal.fromBigNumber(currentBammLUSD).mul(100).div(Decimal.fromBigNumber(totalLusdInSp)) // bamm share in SP times stake div by total - const poolShare = bammShare.mul(stake).div(total) + const poolShare = bammShare.mul(Decimal.fromBigNumber(stake)).div(Decimal.fromBigNumber(total)) - const bammEthBalance = await bammEthBalancePromise.then(decimalify) - const currentETH = stake.mul(bammEthBalance.add(bammPendingEth)).div(total) + const bammEthBalance = await bammEthBalancePromise + const currentETH = (stake.mul(bammEthBalance.add(bammPendingEth))).div(total) // balance + pending - stock - let lqtyReward = Decimal.from(0) - if(total.gt(Decimal.from(0))){ + let lqtyReward = BigNumber.from(0) + if(total.gt(BigNumber.from(0))){ const crop = bammLqtyBalance.add(bammPendingLqtyReward).sub(stock); - const updatedShare = share.add(crop.mul(RAY).mul(_1e18).div(total)) - const updatedCrops = stake.mul(updatedShare).mul(_1e18).div(RAY) + const updatedShare = share.add(crop.mul(RAY).div(total)) + const updatedCrops = stake.mul(updatedShare).div(RAY) console.log( JSON.stringify({ bammLqtyBalance: bammLqtyBalance.toString(), @@ -346,9 +347,9 @@ export class ReadableEthersLiquity implements ReadableLiquity { return new StabilityDeposit( poolShare, decimalify(initialValue), - currentLUSD, - currentETH, - lqtyReward, + Decimal.fromBigNumber(currentLUSD), + Decimal.fromBigNumber(currentETH), + Decimal.fromBigNumber(lqtyReward), frontEndTag ); } From 62155bbe1798001cdc9a99f0979f39c53d08697d Mon Sep 17 00:00:00 2001 From: shmuel Date: Mon, 12 Jul 2021 16:42:14 +0300 Subject: [PATCH 31/90] remove the Claim LQTY and move ETH to Trove BTN --- .../dev-frontend/src/components/Stability/ActiveDeposit.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx b/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx index 77afbb249..d8e63aebe 100644 --- a/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx +++ b/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx @@ -116,9 +116,6 @@ export const ActiveDeposit: React.FC = () => { Claim ETH and LQTY - {hasTrove && ( - Claim LQTY and move ETH to Trove - )} {isWaitingForTransaction && } From d51f03e5d50c1affa566694387e0c2e450d50e44 Mon Sep 17 00:00:00 2001 From: shmuel Date: Mon, 12 Jul 2021 16:43:23 +0300 Subject: [PATCH 32/90] text change --- .../dev-frontend/src/components/Stability/ActiveDeposit.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx b/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx index d8e63aebe..1b38949a7 100644 --- a/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx +++ b/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx @@ -113,7 +113,7 @@ export const ActiveDeposit: React.FC = () => {  Adjust - Claim ETH and LQTY + Claim LQTY From f2ce78ddf5dc89c65332681eaa2d7c6f175897cb Mon Sep 17 00:00:00 2001 From: shmuel Date: Mon, 12 Jul 2021 16:45:15 +0300 Subject: [PATCH 33/90] text change --- .../dev-frontend/src/components/Stability/ActiveDeposit.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx b/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx index 1b38949a7..0f48c9fed 100644 --- a/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx +++ b/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx @@ -75,7 +75,7 @@ export const ActiveDeposit: React.FC = () => { /> Date: Mon, 12 Jul 2021 16:53:05 +0300 Subject: [PATCH 34/90] minor layout change --- .../components/Stability/ActiveDeposit.tsx | 14 +++++----- .../Stability/StabilityDepositEditor.tsx | 28 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx b/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx index 0f48c9fed..f8c90fe42 100644 --- a/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx +++ b/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx @@ -67,13 +67,6 @@ export const ActiveDeposit: React.FC = () => { unit={COIN} /> - - { unit="ETH" /> + + = ({ setEditedAmount={newValue => dispatch({ type: "setDeposit", newValue })} /> - {newPoolShare.infinite ? ( - - ) : ( - - )} - {!originalDeposit.isEmpty && ( <> + {newPoolShare.infinite ? ( + + ) : ( + + )} + Date: Tue, 13 Jul 2021 21:00:46 +0300 Subject: [PATCH 35/90] unlock --- .../src/components/Stability/NoDeposit.tsx | 38 ++++++++++++++++++- packages/lib-base/etc/lib-base.api.md | 4 +- packages/lib-base/src/StabilityDeposit.ts | 6 ++- packages/lib-ethers/etc/lib-ethers.api.md | 4 ++ .../lib-ethers/src/BlockPolledLiquityStore.ts | 3 +- .../src/PopulatableEthersLiquity.ts | 19 ++++++++++ .../lib-ethers/src/ReadableEthersLiquity.ts | 14 +++++-- .../lib-ethers/src/SendableEthersLiquity.ts | 7 ++++ 8 files changed, 86 insertions(+), 9 deletions(-) diff --git a/packages/dev-frontend/src/components/Stability/NoDeposit.tsx b/packages/dev-frontend/src/components/Stability/NoDeposit.tsx index 24ecc9f25..430e5f1d6 100644 --- a/packages/dev-frontend/src/components/Stability/NoDeposit.tsx +++ b/packages/dev-frontend/src/components/Stability/NoDeposit.tsx @@ -1,17 +1,46 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useState, useEffect } from "react"; import { Card, Heading, Box, Flex, Button } from "theme-ui"; import { InfoMessage } from "../InfoMessage"; import { useStabilityView } from "./context/StabilityViewContext"; import { RemainingLQTY } from "./RemainingLQTY"; import { Yield } from "./Yield"; +import { LiquityStoreState } from "@liquity/lib-base"; +import { useLiquitySelector } from "@liquity/lib-react"; +import { useTransactionFunction } from "../Transaction"; +import { useLiquity } from "./../../hooks/LiquityContext"; + +//create your forceUpdate hook +function useForceUpdate(){ + const [value, setValue] = useState(0); // integer state + return () => setValue(value => value + 1); // update the state to force render +} + +const selector = ({ stabilityDeposit }: LiquityStoreState) => ({ + stabilityDeposit +}); export const NoDeposit: React.FC = props => { + const { liquity } = useLiquity(); + const { stabilityDeposit } = useLiquitySelector(selector); const { dispatchEvent } = useStabilityView(); + const [allowanceSucceed, setAllowanceSucceed] = useState(false); const handleOpenTrove = useCallback(() => { dispatchEvent("DEPOSIT_PRESSED"); }, [dispatchEvent]); + const [sendTransaction, transactionState] = useTransactionFunction( + "bamm-unlock", + liquity.send.bammUnlock.bind(liquity.send) + ); + + useEffect(() => { + if (transactionState.type === "confirmed") { + setAllowanceSucceed(true); + } + }, [transactionState.type]); + + const hasAllowance = allowanceSucceed || stabilityDeposit.bammAllowance return ( @@ -29,7 +58,12 @@ export const NoDeposit: React.FC = props => { - + {hasAllowance && + + } + {!hasAllowance && + + } diff --git a/packages/lib-base/etc/lib-base.api.md b/packages/lib-base/etc/lib-base.api.md index f0f32c44a..a4b26f6b0 100644 --- a/packages/lib-base/etc/lib-base.api.md +++ b/packages/lib-base/etc/lib-base.api.md @@ -597,8 +597,10 @@ export interface SentLiquityTransaction | undefined): Decimal; + // (undocumented) + readonly bammAllowance: boolean; readonly collateralGain: Decimal; readonly currentLUSD: Decimal; equals(that: StabilityDeposit): boolean; diff --git a/packages/lib-base/src/StabilityDeposit.ts b/packages/lib-base/src/StabilityDeposit.ts index 0460861a4..977a724d7 100644 --- a/packages/lib-base/src/StabilityDeposit.ts +++ b/packages/lib-base/src/StabilityDeposit.ts @@ -39,6 +39,8 @@ export class StabilityDeposit { */ readonly frontendTag: string; + readonly bammAllowance: boolean; + /** @internal */ constructor( poolShare: Decimal, @@ -46,7 +48,8 @@ export class StabilityDeposit { currentLUSD: Decimal, collateralGain: Decimal, lqtyReward: Decimal, - frontendTag: string + frontendTag: string, + bammAllowance: boolean ) { this.poolShare = poolShare; this.initialLUSD = initialLUSD; @@ -54,6 +57,7 @@ export class StabilityDeposit { this.collateralGain = collateralGain; this.lqtyReward = lqtyReward; this.frontendTag = frontendTag; + this.bammAllowance = bammAllowance; } diff --git a/packages/lib-ethers/etc/lib-ethers.api.md b/packages/lib-ethers/etc/lib-ethers.api.md index 0ec038578..392b788b3 100644 --- a/packages/lib-ethers/etc/lib-ethers.api.md +++ b/packages/lib-ethers/etc/lib-ethers.api.md @@ -337,6 +337,8 @@ export class PopulatableEthersLiquity implements PopulatableLiquity>; // (undocumented) + bammUnlock(overrides?: EthersTransactionOverrides): Promise; + // (undocumented) borrowLUSD(amount: Decimalish, maxBorrowingRate?: Decimalish, overrides?: EthersTransactionOverrides): Promise>; // (undocumented) claimCollateralSurplus(overrides?: EthersTransactionOverrides): Promise>; @@ -542,6 +544,8 @@ export class SendableEthersLiquity implements SendableLiquity>; // (undocumented) + bammUnlock(overrides?: EthersTransactionOverrides): Promise; + // (undocumented) borrowLUSD(amount: Decimalish, maxBorrowingRate?: Decimalish, overrides?: EthersTransactionOverrides): Promise>; // (undocumented) claimCollateralSurplus(overrides?: EthersTransactionOverrides): Promise>; diff --git a/packages/lib-ethers/src/BlockPolledLiquityStore.ts b/packages/lib-ethers/src/BlockPolledLiquityStore.ts index 2e74f74e3..6a19219e9 100644 --- a/packages/lib-ethers/src/BlockPolledLiquityStore.ts +++ b/packages/lib-ethers/src/BlockPolledLiquityStore.ts @@ -156,7 +156,8 @@ export class BlockPolledLiquityStore extends LiquityStore gas.add(50000); const addGasForUnipoolRewardUpdate = (gas: BigNumber) => gas.add(20000); +const simpleAddGas = (gas: BigNumber) => gas.add(1000); + // To get the best entropy available, we'd do something like: // // const bigRandomNumber = () => @@ -1138,6 +1140,23 @@ export class PopulatableEthersLiquity // ); // } + /** {@inheritDoc @liquity/lib-base#PopulatableLiquity.bammUnlock} */ + async bammUnlock( + overrides?: EthersTransactionOverrides + ): Promise { + const { bamm, lusdToken } = _getContracts(this._readable.connection); + const maxAllowance = BigNumber.from("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") + + return this._wrapSimpleTransaction( + await lusdToken.estimateAndPopulate.approve( + { ...overrides }, + simpleAddGas, + bamm.address, + maxAllowance + ) + ); + } + /** {@inheritDoc @liquity/lib-base#PopulatableLiquity.withdrawGainsFromStabilityPool} */ async withdrawGainsFromStabilityPool( overrides?: EthersTransactionOverrides diff --git a/packages/lib-ethers/src/ReadableEthersLiquity.ts b/packages/lib-ethers/src/ReadableEthersLiquity.ts index b84027447..f2542029c 100644 --- a/packages/lib-ethers/src/ReadableEthersLiquity.ts +++ b/packages/lib-ethers/src/ReadableEthersLiquity.ts @@ -280,10 +280,11 @@ export class ReadableEthersLiquity implements ReadableLiquity { ): Promise { const RAY = BigNumber.from(10).pow(27) const _1e18 = BigNumber.from(10).pow(18) + const reallyLargeAllowance = BigNumber.from("0x8888888888888888888888888888888888888888888888888888888888888888") + address ??= _requireAddress(this.connection); - const { stabilityPool, bamm, lqtyToken } = _getContracts(this.connection); + const { stabilityPool, bamm, lqtyToken, lusdToken } = _getContracts(this.connection); const bammLqtyBalancePromise = lqtyToken.balanceOf(bamm.address, { ...overrides}) - const bammEthBalancePromise = bamm.provider.getBalance(bamm.address) const [ { frontEndTag, initialValue }, @@ -317,7 +318,7 @@ export class ReadableEthersLiquity implements ReadableLiquity { // bamm share in SP times stake div by total const poolShare = bammShare.mul(Decimal.fromBigNumber(stake)).div(Decimal.fromBigNumber(total)) - const bammEthBalance = await bammEthBalancePromise + const bammEthBalance = await bamm.provider.getBalance(bamm.address) const currentETH = (stake.mul(bammEthBalance.add(bammPendingEth))).div(total) // balance + pending - stock @@ -344,13 +345,18 @@ export class ReadableEthersLiquity implements ReadableLiquity { lqtyReward = updatedCrops.sub(crops) } } + + const allowance = await lusdToken.allowance(address, bamm.address) + console.log({allowance}) + const bammAllowance = allowance.gt(reallyLargeAllowance) return new StabilityDeposit( poolShare, decimalify(initialValue), Decimal.fromBigNumber(currentLUSD), Decimal.fromBigNumber(currentETH), Decimal.fromBigNumber(lqtyReward), - frontEndTag + frontEndTag, + bammAllowance ); } diff --git a/packages/lib-ethers/src/SendableEthersLiquity.ts b/packages/lib-ethers/src/SendableEthersLiquity.ts index 225632632..689b8fc3f 100644 --- a/packages/lib-ethers/src/SendableEthersLiquity.ts +++ b/packages/lib-ethers/src/SendableEthersLiquity.ts @@ -155,6 +155,13 @@ export class SendableEthersLiquity return this._populate.withdrawGainsFromStabilityPool(overrides).then(sendTransaction); } + /** {@inheritDoc @liquity/lib-base#SendableLiquity.bammUnlock} */ + bammUnlock( + overrides?: EthersTransactionOverrides + ): Promise { + return this._populate.bammUnlock(overrides).then(sendTransaction); + } + /** {@inheritDoc @liquity/lib-base#SendableLiquity.transferCollateralGainToTrove} */ transferCollateralGainToTrove( overrides?: EthersTransactionOverrides From 1dc6bc74eb3c9121a32dc560dd9c5fffede1199a Mon Sep 17 00:00:00 2001 From: shmuel Date: Wed, 14 Jul 2021 14:32:01 +0300 Subject: [PATCH 36/90] bamm pool share --- .../components/Stability/ActiveDeposit.tsx | 11 +++++- .../Stability/StabilityDepositEditor.tsx | 37 +++++++++++++++++-- packages/lib-base/etc/lib-base.api.md | 3 +- packages/lib-base/src/StabilityDeposit.ts | 5 +++ .../lib-ethers/src/BlockPolledLiquityStore.ts | 1 + .../lib-ethers/src/ReadableEthersLiquity.ts | 2 + 6 files changed, 52 insertions(+), 7 deletions(-) diff --git a/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx b/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx index f8c90fe42..8ba458f5a 100644 --- a/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx +++ b/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx @@ -26,7 +26,7 @@ export const ActiveDeposit: React.FC = () => { const { dispatchEvent } = useStabilityView(); const { stabilityDeposit, trove, lusdInStabilityPool } = useLiquitySelector(selector); - const poolShare = stabilityDeposit.poolShare + const {poolShare, bammPoolShare} = stabilityDeposit const handleAdjustDeposit = useCallback(() => { dispatchEvent("ADJUST_DEPOSIT_PRESSED"); @@ -68,7 +68,7 @@ export const ActiveDeposit: React.FC = () => { /> { unit="%" /> + + ({ +const select = ({ lusdBalance, lusdInStabilityPool, stabilityDeposit }: LiquityStoreState) => ({ lusdBalance, - lusdInStabilityPool + lusdInStabilityPool, + stabilityDeposit }); type StabilityDepositEditorProps = { @@ -30,6 +31,8 @@ type StabilityDepositEditorProps = { dispatch: (action: { type: "setDeposit"; newValue: Decimalish } | { type: "revert" }) => void; }; +const selectPrice = ({ price }: LiquityStoreState) => price; + export const StabilityDepositEditor: React.FC = ({ originalDeposit, editedLUSD, @@ -37,8 +40,9 @@ export const StabilityDepositEditor: React.FC = ({ dispatch, children }) => { - const { lusdBalance, lusdInStabilityPool } = useLiquitySelector(select); + const { lusdBalance, lusdInStabilityPool, stabilityDeposit } = useLiquitySelector(select); const editingState = useState(); + const price = useLiquitySelector(selectPrice); const edited = !editedLUSD.eq(originalDeposit.currentLUSD); @@ -55,6 +59,18 @@ export const StabilityDepositEditor: React.FC = ({ originalDeposit.currentLUSD.nonZero && Difference.between(newPoolShare, originalPoolShare).nonZero; + const {bammPoolShare, collateralGain} = stabilityDeposit; + + const userTotalUsdInBamm = (originalDeposit.currentLUSD.add(collateralGain.mul(price))) + const totalLusdInBamm = userTotalUsdInBamm.mulDiv(100, bammPoolShare); + const editedUserLusd = userTotalUsdInBamm.sub(originalDeposit.currentLUSD).add(editedLUSD); + const editedTotalLusd = totalLusdInBamm.sub(originalDeposit.currentLUSD).add(editedLUSD); + const editedBammPoolShare = editedUserLusd.mulDiv(100, editedTotalLusd) + + const bammPoolShareChange = + originalDeposit.currentLUSD.nonZero && + Difference.between(editedBammPoolShare, bammPoolShare).nonZero; + return ( @@ -86,7 +102,7 @@ export const StabilityDepositEditor: React.FC = ({ {!originalDeposit.isEmpty && ( <> = ({ unit="%" /> )} + + {bammPoolShare.infinite ? ( + + ) : ( + + )} | undefined): Decimal; // (undocumented) readonly bammAllowance: boolean; + readonly bammPoolShare: Decimal; readonly collateralGain: Decimal; readonly currentLUSD: Decimal; equals(that: StabilityDeposit): boolean; diff --git a/packages/lib-base/src/StabilityDeposit.ts b/packages/lib-base/src/StabilityDeposit.ts index 977a724d7..6cab4203e 100644 --- a/packages/lib-base/src/StabilityDeposit.ts +++ b/packages/lib-base/src/StabilityDeposit.ts @@ -15,6 +15,9 @@ export type StabilityDepositChange = * @public */ export class StabilityDeposit { + /** pool share of user in the BAMM wich has a share in the stability pool */ + readonly bammPoolShare: Decimal; + /** pool share of user in the BAMM wich has a share in the stability pool */ readonly poolShare: Decimal; @@ -43,6 +46,7 @@ export class StabilityDeposit { /** @internal */ constructor( + bammPoolShare: Decimal, poolShare: Decimal, initialLUSD: Decimal, currentLUSD: Decimal, @@ -51,6 +55,7 @@ export class StabilityDeposit { frontendTag: string, bammAllowance: boolean ) { + this.bammPoolShare = bammPoolShare; this.poolShare = poolShare; this.initialLUSD = initialLUSD; this.currentLUSD = currentLUSD; diff --git a/packages/lib-ethers/src/BlockPolledLiquityStore.ts b/packages/lib-ethers/src/BlockPolledLiquityStore.ts index 6a19219e9..2624b230e 100644 --- a/packages/lib-ethers/src/BlockPolledLiquityStore.ts +++ b/packages/lib-ethers/src/BlockPolledLiquityStore.ts @@ -156,6 +156,7 @@ export class BlockPolledLiquityStore extends LiquityStore Date: Thu, 15 Jul 2021 12:20:23 +0300 Subject: [PATCH 37/90] lens --- .../contracts/contracts/B.Protocol/BLens.sol | 58 +++++++++++++++++++ .../contracts/test/B.Protocol/BAMMTest.js | 8 +++ .../contracts/test/B.Protocol/PickleTest.js | 8 +++ 3 files changed, 74 insertions(+) create mode 100644 packages/contracts/contracts/B.Protocol/BLens.sol diff --git a/packages/contracts/contracts/B.Protocol/BLens.sol b/packages/contracts/contracts/B.Protocol/BLens.sol new file mode 100644 index 000000000..739835385 --- /dev/null +++ b/packages/contracts/contracts/B.Protocol/BLens.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.11; + +import "./BAMM.sol"; +import "./../Dependencies/SafeMath.sol"; + + +contract BLens { + function add(uint256 x, uint256 y) public pure returns (uint256 z) { + require((z = x + y) >= x, "ds-math-add-overflow"); + } + function sub(uint256 x, uint256 y) public pure returns (uint256 z) { + require((z = x - y) <= x, "ds-math-sub-underflow"); + } + function mul(uint256 x, uint256 y) public pure returns (uint256 z) { + require(y == 0 || (z = x * y) / y == x, "ds-math-mul-overflow"); + } + function divup(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = add(x, sub(y, 1)) / y; + } + uint256 constant WAD = 10 ** 18; + function wmul(uint256 x, uint256 y) public pure returns (uint256 z) { + z = mul(x, y) / WAD; + } + function wdiv(uint256 x, uint256 y) public pure returns (uint256 z) { + z = mul(x, WAD) / y; + } + function wdivup(uint256 x, uint256 y) public pure returns (uint256 z) { + z = divup(mul(x, WAD), y); + } + uint256 constant RAY = 10 ** 27; + function rmul(uint256 x, uint256 y) public pure returns (uint256 z) { + z = mul(x, y) / RAY; + } + function rmulup(uint256 x, uint256 y) public pure returns (uint256 z) { + z = divup(mul(x, y), RAY); + } + function rdiv(uint256 x, uint256 y) public pure returns (uint256 z) { + z = mul(x, RAY) / y; + } + + function getUnclaimedLqty(address user, BAMM bamm, ERC20 token) external returns(uint) { + // trigger bamm (p)lqty claim + bamm.withdraw(0); + + if(bamm.total() == 0) return 0; + + // duplicate harvest logic + uint crop = sub(token.balanceOf(address(bamm)), bamm.stock()); + uint share = add(bamm.share(), rdiv(crop, bamm.total())); + + uint last = bamm.crops(user); + uint curr = rmul(bamm.stake(user), share); + if(curr > last) return curr - last; + return 0; + } +} diff --git a/packages/contracts/test/B.Protocol/BAMMTest.js b/packages/contracts/test/B.Protocol/BAMMTest.js index df8a028e6..3674ca5e9 100644 --- a/packages/contracts/test/B.Protocol/BAMMTest.js +++ b/packages/contracts/test/B.Protocol/BAMMTest.js @@ -10,6 +10,7 @@ const TroveManagerTester = artifacts.require("TroveManagerTester") const LUSDToken = artifacts.require("LUSDToken") const NonPayable = artifacts.require('NonPayable.sol') const BAMM = artifacts.require("BAMM.sol") +const BLens = artifacts.require("BLens.sol") const ChainlinkTestnet = artifacts.require("ChainlinkTestnet.sol") const ZERO = toBN('0') @@ -43,6 +44,7 @@ contract('BAMM', async accounts => { let activePool let stabilityPool let bamm + let lens let chainlink let defaultPool let borrowerOperations @@ -100,6 +102,7 @@ contract('BAMM', async accounts => { await stabilityPool.registerFrontEnd(kickbackRate_F1, { from: frontEnd_1 }) bamm = await BAMM.new(chainlink.address, stabilityPool.address, lusdToken.address, lqtyToken.address, 400, feePool, frontEnd_1, {from: bammOwner}) + lens = await BLens.new() }) // --- provideToSP() --- @@ -250,6 +253,9 @@ contract('BAMM', async accounts => { await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + const expectdDLqtyDelta = await lens.getUnclaimedLqty.call(D, bamm.address, lqtyToken.address) + const expectdELqtyDelta = await lens.getUnclaimedLqty.call(E, bamm.address, lqtyToken.address) + await stabilityPool.withdrawFromSP(0, { from: F }) await bamm.withdraw(0, { from: D }) await bamm.withdraw(0, { from: E }) @@ -260,6 +266,8 @@ contract('BAMM', async accounts => { const F_LQTYBalance_After = await lqtyToken.balanceOf(F) assert((await lqtyToken.balanceOf(frontEnd_1)).gt(toBN(0))) + assert.equal(D_LQTYBalance_After.sub(D_LQTYBalance_Before).toString(), expectdDLqtyDelta.toString()) + assert.equal(E_LQTYBalance_After.sub(E_LQTYBalance_Before).toString(), expectdELqtyDelta.toString()) assert.equal(D_LQTYBalance_After.add(E_LQTYBalance_After).toString(), F_LQTYBalance_After.toString()) }) diff --git a/packages/contracts/test/B.Protocol/PickleTest.js b/packages/contracts/test/B.Protocol/PickleTest.js index 582e5f154..81893eb34 100644 --- a/packages/contracts/test/B.Protocol/PickleTest.js +++ b/packages/contracts/test/B.Protocol/PickleTest.js @@ -10,6 +10,7 @@ const TroveManagerTester = artifacts.require("TroveManagerTester") const LUSDToken = artifacts.require("LUSDToken") const NonPayable = artifacts.require('NonPayable.sol') const BAMM = artifacts.require("PBAMM.sol") +const BLens = artifacts.require("BLens.sol") const EIP20 = artifacts.require("EIP20.sol") const Pickle = artifacts.require("PickleJar.sol") const ChainlinkTestnet = artifacts.require("ChainlinkTestnet.sol") @@ -107,6 +108,7 @@ contract('Pickle', async accounts => { pJar = await Pickle.new(lqtyToken.address, pLqty.address) bamm = await BAMM.new(chainlink.address, stabilityPool.address, lusdToken.address, lqtyToken.address, 400, feePool, frontEnd_1, pLqty.address, pJar.address, {from: bammOwner}) + lens = await BLens.new() }) // --- provideToSP() --- @@ -257,6 +259,9 @@ contract('Pickle', async accounts => { await th.fastForwardTime(timeValues.SECONDS_IN_ONE_HOUR, web3.currentProvider) + const expectdDLqtyDelta = await lens.getUnclaimedLqty.call(D, bamm.address, pLqty.address) + const expectdELqtyDelta = await lens.getUnclaimedLqty.call(E, bamm.address, pLqty.address) + await stabilityPool.withdrawFromSP(0, { from: F }) await bamm.withdraw(0, { from: D }) await bamm.withdraw(0, { from: E }) @@ -270,6 +275,9 @@ contract('Pickle', async accounts => { assert((await pLqty.balanceOf(D)).gt(toBN(0))) assert((await pLqty.balanceOf(E)).gt(toBN(0))) + assert.equal(D_LQTYBalance_After.sub(D_LQTYBalance_Before).toString(), expectdDLqtyDelta.toString()) + assert.equal(E_LQTYBalance_After.sub(E_LQTYBalance_Before).toString(), expectdELqtyDelta.toString()) + assert.equal(D_LQTYBalance_After.add(E_LQTYBalance_After).toString(), F_LQTYBalance_After.div(toBN(2)).toString()) }) From 03847c810581a54db6cfe6717b98b2a741fcdcb3 Mon Sep 17 00:00:00 2001 From: shmuel Date: Mon, 19 Jul 2021 10:07:46 +0300 Subject: [PATCH 38/90] working state + LUSD balance + ETH balance + input validation --- .../components/Stability/ActiveDeposit.tsx | 23 +++-- .../Stability/StabilityDepositEditor.tsx | 93 +++++++++++++------ .../Stability/StabilityDepositManager.tsx | 20 ++-- packages/lib-base/etc/lib-base.api.md | 11 ++- packages/lib-base/src/Decimal.ts | 9 ++ packages/lib-base/src/StabilityDeposit.ts | 28 ++++-- .../lib-ethers/src/BlockPolledLiquityStore.ts | 5 +- .../lib-ethers/src/ReadableEthersLiquity.ts | 16 +++- 8 files changed, 144 insertions(+), 61 deletions(-) diff --git a/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx b/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx index 8ba458f5a..26bad95a1 100644 --- a/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx +++ b/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx @@ -63,17 +63,24 @@ export const ActiveDeposit: React.FC = () => { + + - + + void; }; @@ -35,7 +35,7 @@ const selectPrice = ({ price }: LiquityStoreState) => price; export const StabilityDepositEditor: React.FC = ({ originalDeposit, - editedLUSD, + editedUSD, changePending, dispatch, children @@ -44,33 +44,61 @@ export const StabilityDepositEditor: React.FC = ({ const editingState = useState(); const price = useLiquitySelector(selectPrice); - const edited = !editedLUSD.eq(originalDeposit.currentLUSD); + const edited = !editedUSD.eq(stabilityDeposit.currentUSD); - const maxAmount = originalDeposit.currentLUSD.add(lusdBalance); - const maxedOut = editedLUSD.eq(maxAmount); - - const lusdInStabilityPoolAfterChange = lusdInStabilityPool - .sub(originalDeposit.currentLUSD) - .add(editedLUSD); + const maxAmount = stabilityDeposit.currentUSD.add(lusdBalance); + const maxedOut = editedUSD.eq(maxAmount); + const ethInUsd = originalDeposit.currentUSD.sub(stabilityDeposit.currentLUSD) + const originalPoolShare = originalDeposit.currentLUSD.mulDiv(100, lusdInStabilityPool); - const newPoolShare = editedLUSD.mulDiv(100, lusdInStabilityPoolAfterChange); - const poolShareChange = - originalDeposit.currentLUSD.nonZero && - Difference.between(newPoolShare, originalPoolShare).nonZero; const {bammPoolShare, collateralGain} = stabilityDeposit; - const userTotalUsdInBamm = (originalDeposit.currentLUSD.add(collateralGain.mul(price))) - const totalLusdInBamm = userTotalUsdInBamm.mulDiv(100, bammPoolShare); - const editedUserLusd = userTotalUsdInBamm.sub(originalDeposit.currentLUSD).add(editedLUSD); - const editedTotalLusd = totalLusdInBamm.sub(originalDeposit.currentLUSD).add(editedLUSD); - const editedBammPoolShare = editedUserLusd.mulDiv(100, editedTotalLusd) + const userTotalUsdInBamm = stabilityDeposit.currentUSD + const totalUsdInBamm = userTotalUsdInBamm.mulDiv(100, bammPoolShare); + const editedUserUsd = userTotalUsdInBamm.sub(stabilityDeposit.currentUSD).add(editedUSD); + const editedTotalUsdInBamm = totalUsdInBamm.sub(stabilityDeposit.currentUSD).add(editedUSD); + const editedBammPoolShare = editedUserUsd.mulDiv(100, editedTotalUsdInBamm) + + /* USD balance + ====================================================================*/ + const usdDiff = Difference.between(editedUSD, stabilityDeposit.currentUSD) const bammPoolShareChange = - originalDeposit.currentLUSD.nonZero && + stabilityDeposit.currentUSD.nonZero && Difference.between(editedBammPoolShare, bammPoolShare).nonZero; - + + let newTotalLusd, newTotalEth; + if(bammPoolShareChange && !bammPoolShareChange?.nonZero || bammPoolShareChange?.positive){ + newTotalLusd = stabilityDeposit.totalLusdInBamm.add(Decimal.from(usdDiff.absoluteValue||0)); + newTotalEth = stabilityDeposit.totalEthInBamm; + } else { + newTotalLusd = stabilityDeposit.totalLusdInBamm.mul((editedTotalUsdInBamm.div(totalUsdInBamm))) + newTotalEth = stabilityDeposit.totalEthInBamm.mul((editedTotalUsdInBamm.div(totalUsdInBamm))) + } + + /* ETH balance + ====================================================================*/ + const newEthBalance = editedBammPoolShare.mul(newTotalEth).div(100) + let ethDiff = Difference.between(newEthBalance, stabilityDeposit.collateralGain).nonZero + + /* LUSD balance + ====================================================================*/ + const newLusdBalance = editedBammPoolShare.mul(newTotalLusd).div(100) + let lusdDiff = Difference.between(newLusdBalance, stabilityDeposit.currentLUSD).nonZero + + + /* pool share + ====================================================================*/ + const lusdInStabilityPoolAfterChange = lusdInStabilityPool + .add(newTotalLusd) + .sub(stabilityDeposit.totalLusdInBamm); + + const newPoolShare = (newTotalLusd.mulDiv(editedBammPoolShare, 100)).mulDiv(100, lusdInStabilityPoolAfterChange); + const poolShareChange = + originalDeposit.currentLUSD.nonZero && + Difference.between(newPoolShare, originalPoolShare).nonZero; return ( @@ -90,25 +118,36 @@ export const StabilityDepositEditor: React.FC = ({ dispatch({ type: "setDeposit", newValue })} /> {!originalDeposit.isEmpty && ( <> + + + - + {newPoolShare.infinite ? ( ) : ( diff --git a/packages/dev-frontend/src/components/Stability/StabilityDepositManager.tsx b/packages/dev-frontend/src/components/Stability/StabilityDepositManager.tsx index a7b8bd256..aea757956 100644 --- a/packages/dev-frontend/src/components/Stability/StabilityDepositManager.tsx +++ b/packages/dev-frontend/src/components/Stability/StabilityDepositManager.tsx @@ -19,7 +19,7 @@ import { const init = ({ stabilityDeposit }: LiquityStoreState) => ({ originalDeposit: stabilityDeposit, - editedLUSD: stabilityDeposit.currentLUSD, + editedUSD: stabilityDeposit.currentUSD, changePending: false }); @@ -40,10 +40,8 @@ const reduce = ( state: StabilityDepositManagerState, action: StabilityDepositManagerAction ): StabilityDepositManagerState => { - // console.log(state); - // console.log(action); - const { originalDeposit, editedLUSD, changePending } = state; + const { originalDeposit, editedUSD, changePending } = state; switch (action.type) { case "startChange": { @@ -55,10 +53,10 @@ const reduce = ( return { ...state, changePending: false }; case "setDeposit": - return { ...state, editedLUSD: Decimal.from(action.newValue) }; + return { ...state, editedUSD: Decimal.from(action.newValue) }; case "revert": - return { ...state, editedLUSD: originalDeposit.currentLUSD }; + return { ...state, editedUSD: originalDeposit.currentUSD }; case "updateStore": { const { @@ -83,7 +81,7 @@ const reduce = ( return { ...newState, - editedLUSD: updatedDeposit.apply(originalDeposit.whatChanged(editedLUSD)) + editedUSD: updatedDeposit.apply(originalDeposit.whatChanged(editedUSD)) }; } } @@ -92,17 +90,17 @@ const reduce = ( const transactionId = "stability-deposit"; export const StabilityDepositManager: React.FC = () => { - const [{ originalDeposit, editedLUSD, changePending }, dispatch] = useLiquityReducer(reduce, init); + const [{ originalDeposit, editedUSD, changePending }, dispatch] = useLiquityReducer(reduce, init); const validationContext = useLiquitySelector(selectForStabilityDepositChangeValidation); const { dispatchEvent } = useStabilityView(); const handleCancel = useCallback(() => { dispatchEvent("CANCEL_PRESSED"); }, [dispatchEvent]); - + debugger const [validChange, description] = validateStabilityDepositChange( originalDeposit, - editedLUSD, + editedUSD, validationContext ); @@ -126,7 +124,7 @@ export const StabilityDepositManager: React.FC = () => { return ( diff --git a/packages/lib-base/etc/lib-base.api.md b/packages/lib-base/etc/lib-base.api.md index c879a8aa2..d5b034961 100644 --- a/packages/lib-base/etc/lib-base.api.md +++ b/packages/lib-base/etc/lib-base.api.md @@ -165,6 +165,8 @@ export class Difference { // (undocumented) static between(d1: Decimalish | undefined, d2: Decimalish | undefined): Difference; // (undocumented) + div(divider: Decimalish): Difference; + // (undocumented) get finite(): this | undefined; // (undocumented) get infinite(): this | undefined; @@ -597,13 +599,14 @@ export interface SentLiquityTransaction | undefined): Decimal; // (undocumented) readonly bammAllowance: boolean; readonly bammPoolShare: Decimal; readonly collateralGain: Decimal; readonly currentLUSD: Decimal; + readonly currentUSD: Decimal; equals(that: StabilityDeposit): boolean; readonly frontendTag: string; readonly initialLUSD: Decimal; @@ -613,7 +616,11 @@ export class StabilityDeposit { readonly poolShare: Decimal; // @internal (undocumented) toString(): string; - whatChanged(thatLUSD: Decimalish): StabilityDepositChange | undefined; + // (undocumented) + readonly totalEthInBamm: Decimal; + // (undocumented) + readonly totalLusdInBamm: Decimal; + whatChanged(thatUSD: Decimalish): StabilityDepositChange | undefined; } // @public diff --git a/packages/lib-base/src/Decimal.ts b/packages/lib-base/src/Decimal.ts index 586c8fe25..4d35fe885 100644 --- a/packages/lib-base/src/Decimal.ts +++ b/packages/lib-base/src/Decimal.ts @@ -400,6 +400,15 @@ export class Difference { ); } + div(divider: Decimalish): Difference { + return new Difference( + this._number && { + sign: this._number.sign, + absoluteValue: this._number.absoluteValue.div(divider) + } + ); + } + get nonZero(): this | undefined { return this._number?.absoluteValue.nonZero && this; } diff --git a/packages/lib-base/src/StabilityDeposit.ts b/packages/lib-base/src/StabilityDeposit.ts index 6cab4203e..0f5fbdff2 100644 --- a/packages/lib-base/src/StabilityDeposit.ts +++ b/packages/lib-base/src/StabilityDeposit.ts @@ -27,6 +27,9 @@ export class StabilityDeposit { /** Amount of LUSD left in the Stability Deposit. */ readonly currentLUSD: Decimal; + /** Amount of USD left in the Stability Deposit. */ + readonly currentUSD: Decimal; + /** Amount of native currency (e.g. Ether) received in exchange for the used-up LUSD. */ readonly collateralGain: Decimal; @@ -44,26 +47,35 @@ export class StabilityDeposit { readonly bammAllowance: boolean; + readonly totalEthInBamm: Decimal; + + readonly totalLusdInBamm: Decimal; + /** @internal */ constructor( bammPoolShare: Decimal, poolShare: Decimal, initialLUSD: Decimal, + currentUSD: Decimal, currentLUSD: Decimal, collateralGain: Decimal, lqtyReward: Decimal, frontendTag: string, - bammAllowance: boolean + bammAllowance: boolean, + totalEthInBamm: Decimal, + totalLusdInBamm: Decimal ) { this.bammPoolShare = bammPoolShare; this.poolShare = poolShare; this.initialLUSD = initialLUSD; + this.currentUSD = currentUSD; this.currentLUSD = currentLUSD; this.collateralGain = collateralGain; this.lqtyReward = lqtyReward; this.frontendTag = frontendTag; this.bammAllowance = bammAllowance; - + this.totalEthInBamm = totalEthInBamm; + this.totalLusdInBamm = totalLusdInBamm; } get isEmpty(): boolean { @@ -104,15 +116,15 @@ export class StabilityDeposit { * * @returns An object representing the change, or `undefined` if the deposited amounts are equal. */ - whatChanged(thatLUSD: Decimalish): StabilityDepositChange | undefined { - thatLUSD = Decimal.from(thatLUSD); + whatChanged(thatUSD: Decimalish): StabilityDepositChange | undefined { + thatUSD = Decimal.from(thatUSD); - if (thatLUSD.lt(this.currentLUSD)) { - return { withdrawLUSD: this.currentLUSD.sub(thatLUSD), withdrawAllLUSD: thatLUSD.isZero }; + if (thatUSD.lt(this.currentUSD)) { + return { withdrawLUSD: this.currentUSD.sub(thatUSD), withdrawAllLUSD: thatUSD.isZero }; } - if (thatLUSD.gt(this.currentLUSD)) { - return { depositLUSD: thatLUSD.sub(this.currentLUSD) }; + if (thatUSD.gt(this.currentUSD)) { + return { depositLUSD: thatUSD.sub(this.currentUSD) }; } } diff --git a/packages/lib-ethers/src/BlockPolledLiquityStore.ts b/packages/lib-ethers/src/BlockPolledLiquityStore.ts index 2624b230e..5a2029dac 100644 --- a/packages/lib-ethers/src/BlockPolledLiquityStore.ts +++ b/packages/lib-ethers/src/BlockPolledLiquityStore.ts @@ -157,8 +157,11 @@ export class BlockPolledLiquityStore extends LiquityStore Date: Mon, 19 Jul 2021 10:11:56 +0300 Subject: [PATCH 39/90] styleFix: whale friendly layout :) --- .../dev-frontend/src/components/Stability/ActiveDeposit.tsx | 2 +- .../src/components/Stability/StabilityDepositEditor.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx b/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx index 26bad95a1..a433bdc4d 100644 --- a/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx +++ b/packages/dev-frontend/src/components/Stability/ActiveDeposit.tsx @@ -66,7 +66,7 @@ export const ActiveDeposit: React.FC = () => { amount={stabilityDeposit.currentUSD.prettify()} unit={COIN} /> - + = ({ {!originalDeposit.isEmpty && ( <> - + Date: Mon, 19 Jul 2021 10:21:58 +0300 Subject: [PATCH 40/90] hide away really small rounding errors in the LUSD balance --- .../src/components/Stability/StabilityDepositEditor.tsx | 2 +- packages/lib-base/etc/lib-base.api.md | 2 ++ packages/lib-base/src/Decimal.ts | 8 ++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/dev-frontend/src/components/Stability/StabilityDepositEditor.tsx b/packages/dev-frontend/src/components/Stability/StabilityDepositEditor.tsx index f392b0b1d..8fb6b9d5c 100644 --- a/packages/dev-frontend/src/components/Stability/StabilityDepositEditor.tsx +++ b/packages/dev-frontend/src/components/Stability/StabilityDepositEditor.tsx @@ -86,7 +86,7 @@ export const StabilityDepositEditor: React.FC = ({ /* LUSD balance ====================================================================*/ const newLusdBalance = editedBammPoolShare.mul(newTotalLusd).div(100) - let lusdDiff = Difference.between(newLusdBalance, stabilityDeposit.currentLUSD).nonZero + let lusdDiff = Difference.between(newLusdBalance, stabilityDeposit.currentLUSD).nonZeroish(17) /* pool share diff --git a/packages/lib-base/etc/lib-base.api.md b/packages/lib-base/etc/lib-base.api.md index d5b034961..da8645009 100644 --- a/packages/lib-base/etc/lib-base.api.md +++ b/packages/lib-base/etc/lib-base.api.md @@ -177,6 +177,8 @@ export class Difference { // (undocumented) get nonZero(): this | undefined; // (undocumented) + nonZeroish(precision: number): this | undefined; + // (undocumented) get positive(): this | undefined; // (undocumented) prettify(precision?: number): string; diff --git a/packages/lib-base/src/Decimal.ts b/packages/lib-base/src/Decimal.ts index 4d35fe885..83fbc9fa3 100644 --- a/packages/lib-base/src/Decimal.ts +++ b/packages/lib-base/src/Decimal.ts @@ -413,6 +413,14 @@ export class Difference { return this._number?.absoluteValue.nonZero && this; } + nonZeroish(precision: number): this | undefined { + const zeroish = `0.${"0".repeat(precision)}5`; + + if (this._number?.absoluteValue.gte(zeroish)) { + return this; + } + } + get positive(): this | undefined { return this._number?.sign === "+" ? this : undefined; } From 054e632a6d318cff96d91d17bc0037b1e2099ed7 Mon Sep 17 00:00:00 2001 From: shmuel Date: Mon, 19 Jul 2021 12:27:29 +0300 Subject: [PATCH 41/90] adding bLens contract and reading LQTY reward from it --- packages/lib-ethers/abi/ActivePool.json | 13 + packages/lib-ethers/abi/BAMM.json | 20 +- packages/lib-ethers/abi/BLens.json | 247 ++++++++++++++++++ .../lib-ethers/abi/BorrowerOperations.json | 13 + packages/lib-ethers/abi/CollSurplusPool.json | 13 + .../lib-ethers/abi/CommunityIssuance.json | 13 + packages/lib-ethers/abi/DefaultPool.json | 13 + packages/lib-ethers/abi/HintHelpers.json | 13 + packages/lib-ethers/abi/LQTYStaking.json | 13 + .../lib-ethers/abi/LockupContractFactory.json | 13 + packages/lib-ethers/abi/PriceFeed.json | 13 + packages/lib-ethers/abi/SortedTroves.json | 13 + packages/lib-ethers/abi/StabilityPool.json | 13 + packages/lib-ethers/abi/TroveManager.json | 13 + packages/lib-ethers/abi/Unipool.json | 13 + .../deployments/default/goerli.json | 1 + .../lib-ethers/deployments/default/kovan.json | 1 + .../deployments/default/mainnet.json | 1 + .../deployments/default/rinkeby.json | 1 + .../deployments/default/ropsten.json | 1 + packages/lib-ethers/scripts/generate-types.ts | 2 + .../lib-ethers/src/ReadableEthersLiquity.ts | 25 +- packages/lib-ethers/src/contracts.ts | 4 + packages/lib-ethers/types/index.ts | 38 ++- 24 files changed, 490 insertions(+), 20 deletions(-) create mode 100644 packages/lib-ethers/abi/BLens.json diff --git a/packages/lib-ethers/abi/ActivePool.json b/packages/lib-ethers/abi/ActivePool.json index f3022868c..762871169 100644 --- a/packages/lib-ethers/abi/ActivePool.json +++ b/packages/lib-ethers/abi/ActivePool.json @@ -330,6 +330,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "troveManagerAddress", diff --git a/packages/lib-ethers/abi/BAMM.json b/packages/lib-ethers/abi/BAMM.json index 4b3369420..50a744256 100644 --- a/packages/lib-ethers/abi/BAMM.json +++ b/packages/lib-ethers/abi/BAMM.json @@ -932,6 +932,11 @@ "name": "lusdAmount", "type": "uint256" }, + { + "internalType": "uint256", + "name": "minEthReturn", + "type": "uint256" + }, { "internalType": "address payable", "name": "dest", @@ -946,7 +951,7 @@ "type": "uint256" } ], - "stateMutability": "payable", + "stateMutability": "nonpayable", "type": "function" }, { @@ -1032,6 +1037,19 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "vat", diff --git a/packages/lib-ethers/abi/BLens.json b/packages/lib-ethers/abi/BLens.json new file mode 100644 index 000000000..72da28dc8 --- /dev/null +++ b/packages/lib-ethers/abi/BLens.json @@ -0,0 +1,247 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "name": "add", + "outputs": [ + { + "internalType": "uint256", + "name": "z", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "contract BAMM", + "name": "bamm", + "type": "address" + }, + { + "internalType": "contract ERC20", + "name": "token", + "type": "address" + } + ], + "name": "getUnclaimedLqty", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "name": "mul", + "outputs": [ + { + "internalType": "uint256", + "name": "z", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "name": "rdiv", + "outputs": [ + { + "internalType": "uint256", + "name": "z", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "name": "rmul", + "outputs": [ + { + "internalType": "uint256", + "name": "z", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "name": "rmulup", + "outputs": [ + { + "internalType": "uint256", + "name": "z", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "name": "sub", + "outputs": [ + { + "internalType": "uint256", + "name": "z", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "name": "wdiv", + "outputs": [ + { + "internalType": "uint256", + "name": "z", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "name": "wdivup", + "outputs": [ + { + "internalType": "uint256", + "name": "z", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "name": "wmul", + "outputs": [ + { + "internalType": "uint256", + "name": "z", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + } +] \ No newline at end of file diff --git a/packages/lib-ethers/abi/BorrowerOperations.json b/packages/lib-ethers/abi/BorrowerOperations.json index 79cef5c25..d0a08bed1 100644 --- a/packages/lib-ethers/abi/BorrowerOperations.json +++ b/packages/lib-ethers/abi/BorrowerOperations.json @@ -704,6 +704,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "troveManager", diff --git a/packages/lib-ethers/abi/CollSurplusPool.json b/packages/lib-ethers/abi/CollSurplusPool.json index 079eebcce..8294e7028 100644 --- a/packages/lib-ethers/abi/CollSurplusPool.json +++ b/packages/lib-ethers/abi/CollSurplusPool.json @@ -246,6 +246,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "troveManagerAddress", diff --git a/packages/lib-ethers/abi/CommunityIssuance.json b/packages/lib-ethers/abi/CommunityIssuance.json index 77d7b6579..72a82c8a8 100644 --- a/packages/lib-ethers/abi/CommunityIssuance.json +++ b/packages/lib-ethers/abi/CommunityIssuance.json @@ -253,5 +253,18 @@ ], "stateMutability": "view", "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } ] \ No newline at end of file diff --git a/packages/lib-ethers/abi/DefaultPool.json b/packages/lib-ethers/abi/DefaultPool.json index 3b0b1eaff..8f9e8372c 100644 --- a/packages/lib-ethers/abi/DefaultPool.json +++ b/packages/lib-ethers/abi/DefaultPool.json @@ -276,6 +276,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "troveManagerAddress", diff --git a/packages/lib-ethers/abi/HintHelpers.json b/packages/lib-ethers/abi/HintHelpers.json index 65939a316..c346fe33e 100644 --- a/packages/lib-ethers/abi/HintHelpers.json +++ b/packages/lib-ethers/abi/HintHelpers.json @@ -414,6 +414,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "troveManager", diff --git a/packages/lib-ethers/abi/LQTYStaking.json b/packages/lib-ethers/abi/LQTYStaking.json index a2d7f8f55..929675b0f 100644 --- a/packages/lib-ethers/abi/LQTYStaking.json +++ b/packages/lib-ethers/abi/LQTYStaking.json @@ -506,6 +506,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "troveManagerAddress", diff --git a/packages/lib-ethers/abi/LockupContractFactory.json b/packages/lib-ethers/abi/LockupContractFactory.json index 00ddfa8fb..91ef546be 100644 --- a/packages/lib-ethers/abi/LockupContractFactory.json +++ b/packages/lib-ethers/abi/LockupContractFactory.json @@ -195,5 +195,18 @@ "outputs": [], "stateMutability": "nonpayable", "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } ] \ No newline at end of file diff --git a/packages/lib-ethers/abi/PriceFeed.json b/packages/lib-ethers/abi/PriceFeed.json index 06c8a1d45..2318f4634 100644 --- a/packages/lib-ethers/abi/PriceFeed.json +++ b/packages/lib-ethers/abi/PriceFeed.json @@ -256,5 +256,18 @@ ], "stateMutability": "view", "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } ] \ No newline at end of file diff --git a/packages/lib-ethers/abi/SortedTroves.json b/packages/lib-ethers/abi/SortedTroves.json index f8fcbccc9..2146a3818 100644 --- a/packages/lib-ethers/abi/SortedTroves.json +++ b/packages/lib-ethers/abi/SortedTroves.json @@ -430,6 +430,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "troveManager", diff --git a/packages/lib-ethers/abi/StabilityPool.json b/packages/lib-ethers/abi/StabilityPool.json index b2a055efb..da402f988 100644 --- a/packages/lib-ethers/abi/StabilityPool.json +++ b/packages/lib-ethers/abi/StabilityPool.json @@ -1214,6 +1214,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "troveManager", diff --git a/packages/lib-ethers/abi/TroveManager.json b/packages/lib-ethers/abi/TroveManager.json index 31c09aa9d..3a6c96b1f 100644 --- a/packages/lib-ethers/abi/TroveManager.json +++ b/packages/lib-ethers/abi/TroveManager.json @@ -1682,6 +1682,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/packages/lib-ethers/abi/Unipool.json b/packages/lib-ethers/abi/Unipool.json index d5bebb266..a6861b499 100644 --- a/packages/lib-ethers/abi/Unipool.json +++ b/packages/lib-ethers/abi/Unipool.json @@ -370,6 +370,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "uniToken", diff --git a/packages/lib-ethers/deployments/default/goerli.json b/packages/lib-ethers/deployments/default/goerli.json index 89b5ef378..b45c066dd 100644 --- a/packages/lib-ethers/deployments/default/goerli.json +++ b/packages/lib-ethers/deployments/default/goerli.json @@ -23,6 +23,7 @@ "sortedTroves": "0xBf7022bae995A8690A14e59284c48F7dF6b9F6F2", "stabilityPool": "0xC3809338e11f64B6a0b04c06AB3aC180bD781520", "bamm": "0xaB875C981e1ee054b4C8A6F17DE8461a0dB55b80", + "bLens": "0x4E870f95bB5659c45233fBF3d3A1a01a38F84c5a", "gasPool": "0x9440CD7990b122099A8eF59F74668206C660eED5", "unipool": "0xf195b1B4E8F74C91a4D3013626752eB108D7Ed9B", "lusdToken": "0xfBf329Bd38D57e0db8777915d4835341b97052A9", diff --git a/packages/lib-ethers/deployments/default/kovan.json b/packages/lib-ethers/deployments/default/kovan.json index b37e4d4d9..32f85f11c 100644 --- a/packages/lib-ethers/deployments/default/kovan.json +++ b/packages/lib-ethers/deployments/default/kovan.json @@ -23,6 +23,7 @@ "sortedTroves": "0x91656701b33eca6425A239930FccAA842D0E2031", "stabilityPool": "0x04d630Bff6dea193Fd644dEcfC460db249854a02", "bamm": "0x111CA91E821013F2Ab60652adeFB5115b9521CBF", + "bLens": "0x4E870f95bB5659c45233fBF3d3A1a01a38F84c5a", "gasPool": "0xd97E194C0659F0d7b051EdF3E317BF4F7A675770", "unipool": "0xeed1782CaD8bad6eD2072C85e4c5821e67cCf1E1", "lusdToken": "0x0b02b94638daa719290b5214825dA625af08A02F", diff --git a/packages/lib-ethers/deployments/default/mainnet.json b/packages/lib-ethers/deployments/default/mainnet.json index 4611f3787..9a0a1ee65 100644 --- a/packages/lib-ethers/deployments/default/mainnet.json +++ b/packages/lib-ethers/deployments/default/mainnet.json @@ -23,6 +23,7 @@ "sortedTroves": "0x8FdD3fbFEb32b28fb73555518f8b361bCeA741A6", "stabilityPool": "0x66017D22b0f8556afDd19FC67041899Eb65a21bb", "bamm": "0xaB875C981e1ee054b4C8A6F17DE8461a0dB55b80", + "bLens": "0x4E870f95bB5659c45233fBF3d3A1a01a38F84c5a", "gasPool": "0x9555b042F969E561855e5F28cB1230819149A8d9", "unipool": "0xd37a77E71ddF3373a79BE2eBB76B6c4808bDF0d5", "lusdToken": "0x5f98805A4E8be255a32880FDeC7F6728C6568bA0", diff --git a/packages/lib-ethers/deployments/default/rinkeby.json b/packages/lib-ethers/deployments/default/rinkeby.json index 224a377e6..a0ee1a600 100644 --- a/packages/lib-ethers/deployments/default/rinkeby.json +++ b/packages/lib-ethers/deployments/default/rinkeby.json @@ -23,6 +23,7 @@ "sortedTroves": "0xac064890c6343F67450ba7aB97df6De38A8D7da8", "stabilityPool": "0xB8eb11f9eFF55378dfB692296C32DF020f5CC7fF", "bamm": "0xaB875C981e1ee054b4C8A6F17DE8461a0dB55b80", + "bLens": "0x4E870f95bB5659c45233fBF3d3A1a01a38F84c5a", "gasPool": "0xbbb26b40c1B32ba1F342DAbC65234516dd29BB44", "unipool": "0x866b252Acff4c45d6978032865aFb9923D11992A", "lusdToken": "0x9C5AE6852622ddE455B6Fca4C1551FC0352531a3", diff --git a/packages/lib-ethers/deployments/default/ropsten.json b/packages/lib-ethers/deployments/default/ropsten.json index a3b6510c7..7cef94ea4 100644 --- a/packages/lib-ethers/deployments/default/ropsten.json +++ b/packages/lib-ethers/deployments/default/ropsten.json @@ -23,6 +23,7 @@ "sortedTroves": "0xA401f217DB7d84432C98272F637E9d450c6D16f2", "stabilityPool": "0x02dD2a33d0bBF40343CD09941F275978b1cd4ab9", "bamm": "0xaB875C981e1ee054b4C8A6F17DE8461a0dB55b80", + "bLens": "0x4E870f95bB5659c45233fBF3d3A1a01a38F84c5a", "gasPool": "0x8c2706b7bF86576F16Db7C099F5a62E7Ce8F0661", "unipool": "0x8C2C33247A691a98d05B60c2D7448687b6C56a86", "lusdToken": "0x99Fda92878c1d2f1e0971D1937C50CC578A33E3D", diff --git a/packages/lib-ethers/scripts/generate-types.ts b/packages/lib-ethers/scripts/generate-types.ts index fce9940ed..e2a98dfb2 100644 --- a/packages/lib-ethers/scripts/generate-types.ts +++ b/packages/lib-ethers/scripts/generate-types.ts @@ -22,6 +22,7 @@ import PriceFeedTestnet from "../../contracts/artifacts/contracts/TestContracts/ import SortedTroves from "../../contracts/artifacts/contracts/SortedTroves.sol/SortedTroves.json"; import StabilityPool from "../../contracts/artifacts/contracts/StabilityPool.sol/StabilityPool.json"; import BAMM from "../../contracts/artifacts/contracts/B.Protocol/BAMM.sol/BAMM.json"; +import BLens from "../../contracts/artifacts/contracts/B.Protocol/BLens.sol/BLens.json"; import TroveManager from "../../contracts/artifacts/contracts/TroveManager.sol/TroveManager.json"; import Unipool from "../../contracts/artifacts/contracts/LPRewards/Unipool.sol/Unipool.json"; @@ -163,6 +164,7 @@ const contractArtifacts = [ SortedTroves, StabilityPool, BAMM, + BLens, TroveManager, Unipool ]; diff --git a/packages/lib-ethers/src/ReadableEthersLiquity.ts b/packages/lib-ethers/src/ReadableEthersLiquity.ts index 254a86626..cd5a7959a 100644 --- a/packages/lib-ethers/src/ReadableEthersLiquity.ts +++ b/packages/lib-ethers/src/ReadableEthersLiquity.ts @@ -278,12 +278,11 @@ export class ReadableEthersLiquity implements ReadableLiquity { address?: string, overrides?: EthersCallOverrides ): Promise { - const RAY = BigNumber.from(10).pow(27) const _1e18 = BigNumber.from(10).pow(18) const reallyLargeAllowance = BigNumber.from("0x8888888888888888888888888888888888888888888888888888888888888888") address ??= _requireAddress(this.connection); - const { stabilityPool, bamm, lqtyToken, lusdToken, priceFeed } = _getContracts(this.connection); + const { stabilityPool, bamm, lqtyToken, lusdToken, priceFeed, bLens } = _getContracts(this.connection); const bammLqtyBalancePromise = lqtyToken.balanceOf(bamm.address, { ...overrides}) const [ @@ -294,7 +293,7 @@ export class ReadableEthersLiquity implements ReadableLiquity { total, stake, totalLusdInSp, - crops, + unclaimedLqty, share, stock, ] = await Promise.all([ @@ -305,9 +304,10 @@ export class ReadableEthersLiquity implements ReadableLiquity { bamm.total({ ...overrides }), bamm.stake(address, { ...overrides}), stabilityPool.getTotalLUSDDeposits({ ...overrides }), - bamm.crops(address, { ...overrides }), + bLens.callStatic.getUnclaimedLqty(address, bamm.address, lqtyToken.address), bamm.share({ ...overrides }), bamm.stock({ ...overrides}), + ]); const bammLqtyBalance = await bammLqtyBalancePromise @@ -327,31 +327,20 @@ export class ReadableEthersLiquity implements ReadableLiquity { const bammPoolShare = Decimal.fromBigNumber(stake).mulDiv(100, Decimal.fromBigNumber(total)) // balance + pending - stock - let lqtyReward = BigNumber.from(0) if(total.gt(BigNumber.from(0))){ - const crop = bammLqtyBalance.add(bammPendingLqtyReward).sub(stock); - const updatedShare = share.add(crop.mul(RAY).div(total)) - const updatedCrops = stake.mul(updatedShare).div(RAY) console.log( JSON.stringify({ bammPendingEth: bammPendingEth.toString(), bammLqtyBalance: bammLqtyBalance.toString(), bammPendingLqtyReward: bammPendingLqtyReward.toString(), stock: stock.toString(), - crop: crop.toString(), share: share.toString(), - RAY: RAY.toString(), total: total.toString(), - updatedShare: updatedShare.toString(), stake: stake.toString(), - updatedCrops: updatedCrops.toString(), }, null, 2) ) - if(updatedCrops.gt(crops)){ - lqtyReward = updatedCrops.sub(crops) - } } - + const allowance = await lusdToken.allowance(address, bamm.address) console.log({allowance}) const bammAllowance = allowance.gt(reallyLargeAllowance) @@ -362,11 +351,11 @@ export class ReadableEthersLiquity implements ReadableLiquity { Decimal.fromBigNumber(currentUSD), Decimal.fromBigNumber(currentLUSD), Decimal.fromBigNumber(currentETH), - Decimal.fromBigNumber(lqtyReward), + Decimal.fromBigNumber(unclaimedLqty), frontEndTag, bammAllowance, Decimal.fromBigNumber(bammEthBalance), - Decimal.fromBigNumber(currentBammLUSD) + Decimal.fromBigNumber(unclaimedLqty) ); } diff --git a/packages/lib-ethers/src/contracts.ts b/packages/lib-ethers/src/contracts.ts index e2f5c796e..85670510d 100644 --- a/packages/lib-ethers/src/contracts.ts +++ b/packages/lib-ethers/src/contracts.ts @@ -29,6 +29,7 @@ import priceFeedTestnetAbi from "../abi/PriceFeedTestnet.json"; import sortedTrovesAbi from "../abi/SortedTroves.json"; import stabilityPoolAbi from "../abi/StabilityPool.json"; import BAMMAbi from "../abi/BAMM.json"; +import BLensAbi from "../abi/BLens.json"; import gasPoolAbi from "../abi/GasPool.json"; import unipoolAbi from "../abi/Unipool.json"; import iERC20Abi from "../abi/IERC20.json"; @@ -52,6 +53,7 @@ import { SortedTroves, StabilityPool, BAMM, + BLens, GasPool, Unipool, ERC20Mock, @@ -183,6 +185,7 @@ export interface _LiquityContracts { sortedTroves: SortedTroves; stabilityPool: StabilityPool; bamm: BAMM; + bLens: BLens; gasPool: GasPool; unipool: Unipool; uniToken: IERC20 | ERC20Mock; @@ -220,6 +223,7 @@ const getAbi = (priceFeedIsTestnet: boolean, uniTokenIsMock: boolean): LiquityCo sortedTroves: sortedTrovesAbi, stabilityPool: stabilityPoolAbi, bamm: BAMMAbi, + bLens: BLensAbi, gasPool: gasPoolAbi, collSurplusPool: collSurplusPoolAbi, unipool: unipoolAbi, diff --git a/packages/lib-ethers/types/index.ts b/packages/lib-ethers/types/index.ts index 9161c01a2..4b9613150 100644 --- a/packages/lib-ethers/types/index.ts +++ b/packages/lib-ethers/types/index.ts @@ -28,6 +28,7 @@ interface ActivePoolTransactions { increaseLUSDDebt(_amount: BigNumberish, _overrides?: Overrides): Promise; sendETH(_account: string, _amount: BigNumberish, _overrides?: Overrides): Promise; setAddresses(_borrowerOperationsAddress: string, _troveManagerAddress: string, _stabilityPoolAddress: string, _defaultPoolAddress: string, _overrides?: Overrides): Promise; + transferOwnership(newOwner: string, _overrides?: Overrides): Promise; } export interface ActivePool @@ -92,6 +93,7 @@ interface BorrowerOperationsTransactions { openTrove(_maxFeePercentage: BigNumberish, _LUSDAmount: BigNumberish, _upperHint: string, _lowerHint: string, _overrides?: PayableOverrides): Promise; repayLUSD(_LUSDAmount: BigNumberish, _upperHint: string, _lowerHint: string, _overrides?: Overrides): Promise; setAddresses(_troveManagerAddress: string, _activePoolAddress: string, _defaultPoolAddress: string, _stabilityPoolAddress: string, _gasPoolAddress: string, _collSurplusPoolAddress: string, _priceFeedAddress: string, _sortedTrovesAddress: string, _lusdTokenAddress: string, _lqtyStakingAddress: string, _overrides?: Overrides): Promise; + transferOwnership(newOwner: string, _overrides?: Overrides): Promise; withdrawColl(_collWithdrawal: BigNumberish, _upperHint: string, _lowerHint: string, _overrides?: Overrides): Promise; withdrawLUSD(_maxFeePercentage: BigNumberish, _LUSDAmount: BigNumberish, _upperHint: string, _lowerHint: string, _overrides?: Overrides): Promise; } @@ -145,6 +147,7 @@ interface CollSurplusPoolTransactions { accountSurplus(_account: string, _amount: BigNumberish, _overrides?: Overrides): Promise; claimColl(_account: string, _overrides?: Overrides): Promise; setAddresses(_borrowerOperationsAddress: string, _troveManagerAddress: string, _activePoolAddress: string, _overrides?: Overrides): Promise; + transferOwnership(newOwner: string, _overrides?: Overrides): Promise; } export interface CollSurplusPool @@ -183,6 +186,7 @@ interface CommunityIssuanceTransactions { issueLQTY(_overrides?: Overrides): Promise; sendLQTY(_account: string, _LQTYamount: BigNumberish, _overrides?: Overrides): Promise; setAddresses(_lqtyTokenAddress: string, _stabilityPoolAddress: string, _overrides?: Overrides): Promise; + transferOwnership(newOwner: string, _overrides?: Overrides): Promise; } export interface CommunityIssuance @@ -214,6 +218,7 @@ interface DefaultPoolTransactions { increaseLUSDDebt(_amount: BigNumberish, _overrides?: Overrides): Promise; sendETHToActivePool(_amount: BigNumberish, _overrides?: Overrides): Promise; setAddresses(_troveManagerAddress: string, _activePoolAddress: string, _overrides?: Overrides): Promise; + transferOwnership(newOwner: string, _overrides?: Overrides): Promise; } export interface DefaultPool @@ -312,6 +317,7 @@ interface HintHelpersCalls { interface HintHelpersTransactions { setAddresses(_sortedTrovesAddress: string, _troveManagerAddress: string, _overrides?: Overrides): Promise; + transferOwnership(newOwner: string, _overrides?: Overrides): Promise; } export interface HintHelpers @@ -361,6 +367,7 @@ interface LockupContractFactoryCalls { interface LockupContractFactoryTransactions { deployLockupContract(_beneficiary: string, _unlockTime: BigNumberish, _overrides?: Overrides): Promise; setLQTYTokenAddress(_lqtyTokenAddress: string, _overrides?: Overrides): Promise; + transferOwnership(newOwner: string, _overrides?: Overrides): Promise; } export interface LockupContractFactory @@ -446,6 +453,7 @@ interface LQTYStakingTransactions { increaseF_LUSD(_LUSDFee: BigNumberish, _overrides?: Overrides): Promise; setAddresses(_lqtyTokenAddress: string, _lusdTokenAddress: string, _troveManagerAddress: string, _borrowerOperationsAddress: string, _activePoolAddress: string, _overrides?: Overrides): Promise; stake(_LQTYamount: BigNumberish, _overrides?: Overrides): Promise; + transferOwnership(newOwner: string, _overrides?: Overrides): Promise; unstake(_LQTYamount: BigNumberish, _overrides?: Overrides): Promise; } @@ -562,6 +570,7 @@ interface PriceFeedCalls { interface PriceFeedTransactions { fetchPrice(_overrides?: Overrides): Promise; setAddresses(_priceAggregatorAddress: string, _tellorCallerAddress: string, _overrides?: Overrides): Promise; + transferOwnership(newOwner: string, _overrides?: Overrides): Promise; } export interface PriceFeed @@ -618,6 +627,7 @@ interface SortedTrovesTransactions { reInsert(_id: string, _newNICR: BigNumberish, _prevId: string, _nextId: string, _overrides?: Overrides): Promise; remove(_id: string, _overrides?: Overrides): Promise; setParams(_size: BigNumberish, _troveManagerAddress: string, _borrowerOperationsAddress: string, _overrides?: Overrides): Promise; + transferOwnership(newOwner: string, _overrides?: Overrides): Promise; } export interface SortedTroves @@ -688,6 +698,7 @@ interface StabilityPoolTransactions { provideToSP(_amount: BigNumberish, _frontEndTag: string, _overrides?: Overrides): Promise; registerFrontEnd(_kickbackRate: BigNumberish, _overrides?: Overrides): Promise; setAddresses(_borrowerOperationsAddress: string, _troveManagerAddress: string, _activePoolAddress: string, _lusdTokenAddress: string, _sortedTrovesAddress: string, _priceFeedAddress: string, _communityIssuanceAddress: string, _overrides?: Overrides): Promise; + transferOwnership(newOwner: string, _overrides?: Overrides): Promise; withdrawETHGainToTrove(_upperHint: string, _lowerHint: string, _overrides?: Overrides): Promise; withdrawFromSP(_amount: BigNumberish, _overrides?: Overrides): Promise; } @@ -801,8 +812,9 @@ interface BAMMTransactions { nav(_overrides?: Overrides): Promise; nps(_overrides?: Overrides): Promise; setParams(_A: BigNumberish, _fee: BigNumberish, _overrides?: Overrides): Promise; - swap(lusdAmount: BigNumberish, dest: string, _overrides?: PayableOverrides): Promise; + swap(lusdAmount: BigNumberish, minEthReturn: BigNumberish, dest: string, _overrides?: Overrides): Promise; trade(arg0: string, srcAmount: BigNumberish, arg2: string, destAddress: string, arg4: BigNumberish, arg5: boolean, _overrides?: PayableOverrides): Promise; + transferOwnership(newOwner: string, _overrides?: Overrides): Promise; withdraw(numShares: BigNumberish, _overrides?: Overrides): Promise; } @@ -832,6 +844,28 @@ export interface BAMM extractEvents(logs: Log[], name: "UserWithdraw"): _TypedLogDescription<{ user: string; lusdAmount: BigNumber; ethAmount: BigNumber; numShares: BigNumber }>[]; } +interface BLensCalls { + add(x: BigNumberish, y: BigNumberish, _overrides?: CallOverrides): Promise; + mul(x: BigNumberish, y: BigNumberish, _overrides?: CallOverrides): Promise; + rdiv(x: BigNumberish, y: BigNumberish, _overrides?: CallOverrides): Promise; + rmul(x: BigNumberish, y: BigNumberish, _overrides?: CallOverrides): Promise; + rmulup(x: BigNumberish, y: BigNumberish, _overrides?: CallOverrides): Promise; + sub(x: BigNumberish, y: BigNumberish, _overrides?: CallOverrides): Promise; + wdiv(x: BigNumberish, y: BigNumberish, _overrides?: CallOverrides): Promise; + wdivup(x: BigNumberish, y: BigNumberish, _overrides?: CallOverrides): Promise; + wmul(x: BigNumberish, y: BigNumberish, _overrides?: CallOverrides): Promise; +} + +interface BLensTransactions { + getUnclaimedLqty(user: string, bamm: string, token: string, _overrides?: Overrides): Promise; +} + +export interface BLens + extends _TypedLiquityContract { + readonly filters: { + }; +} + interface TroveManagerCalls { BETA(_overrides?: CallOverrides): Promise; BOOTSTRAP_PERIOD(_overrides?: CallOverrides): Promise; @@ -912,6 +946,7 @@ interface TroveManagerTransactions { removeStake(_borrower: string, _overrides?: Overrides): Promise; setAddresses(_borrowerOperationsAddress: string, _activePoolAddress: string, _defaultPoolAddress: string, _stabilityPoolAddress: string, _gasPoolAddress: string, _collSurplusPoolAddress: string, _priceFeedAddress: string, _lusdTokenAddress: string, _sortedTrovesAddress: string, _lqtyTokenAddress: string, _lqtyStakingAddress: string, _overrides?: Overrides): Promise; setTroveStatus(_borrower: string, _num: BigNumberish, _overrides?: Overrides): Promise; + transferOwnership(newOwner: string, _overrides?: Overrides): Promise; updateStakeAndTotalStakes(_borrower: string, _overrides?: Overrides): Promise; updateTroveRewardSnapshots(_borrower: string, _overrides?: Overrides): Promise; } @@ -992,6 +1027,7 @@ interface UnipoolTransactions { claimReward(_overrides?: Overrides): Promise; setParams(_lqtyTokenAddress: string, _uniTokenAddress: string, _duration: BigNumberish, _overrides?: Overrides): Promise; stake(amount: BigNumberish, _overrides?: Overrides): Promise; + transferOwnership(newOwner: string, _overrides?: Overrides): Promise; withdraw(amount: BigNumberish, _overrides?: Overrides): Promise; withdrawAndClaim(_overrides?: Overrides): Promise; } From 20019229a1511182e9ad373251f3b611ce31c993 Mon Sep 17 00:00:00 2001 From: shmuel Date: Mon, 19 Jul 2021 15:01:38 +0300 Subject: [PATCH 42/90] unlock bamm modal --- .../src/components/Stability/NoDeposit.tsx | 58 ++++++++++++++++++- packages/dev-frontend/src/theme.ts | 10 ++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/dev-frontend/src/components/Stability/NoDeposit.tsx b/packages/dev-frontend/src/components/Stability/NoDeposit.tsx index 430e5f1d6..145d1ce79 100644 --- a/packages/dev-frontend/src/components/Stability/NoDeposit.tsx +++ b/packages/dev-frontend/src/components/Stability/NoDeposit.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useState, useEffect } from "react"; -import { Card, Heading, Box, Flex, Button } from "theme-ui"; +import { Card, Heading, Box, Flex, Button, Container, Close, Paragraph } from "theme-ui"; import { InfoMessage } from "../InfoMessage"; import { useStabilityView } from "./context/StabilityViewContext"; import { RemainingLQTY } from "./RemainingLQTY"; @@ -19,11 +19,52 @@ const selector = ({ stabilityDeposit }: LiquityStoreState) => ({ stabilityDeposit }); +interface BammAllowanceModalProps { + sendTransaction: any, + close: any, +} + +const BammAllowanceModal: React.FC = props => { + return ( +
+ + + + Unlock BAMM + + + + + + + + in order to deposit LUSD and gain LQTY & BPRO + you will need to unlock the BAMM smart contract + + + + + + +
+ ) +} + export const NoDeposit: React.FC = props => { const { liquity } = useLiquity(); const { stabilityDeposit } = useLiquitySelector(selector); const { dispatchEvent } = useStabilityView(); const [allowanceSucceed, setAllowanceSucceed] = useState(false); + const [showModal, setShowModal] = useState(false); const handleOpenTrove = useCallback(() => { dispatchEvent("DEPOSIT_PRESSED"); @@ -35,12 +76,21 @@ export const NoDeposit: React.FC = props => { ); useEffect(() => { + if (transactionState.type === "waitingForConfirmation") { + setShowModal(false); + } + if (transactionState.type === "confirmed") { setAllowanceSucceed(true); } }, [transactionState.type]); const hasAllowance = allowanceSucceed || stabilityDeposit.bammAllowance + + const modalProps = { + close: ()=> setShowModal(false), + sendTransaction, + } return ( @@ -58,11 +108,15 @@ export const NoDeposit: React.FC = props => { + {hasAllowance && } {!hasAllowance && - + + } + {showModal && + }
diff --git a/packages/dev-frontend/src/theme.ts b/packages/dev-frontend/src/theme.ts index 0bb8aa6be..e6ae85afa 100644 --- a/packages/dev-frontend/src/theme.ts +++ b/packages/dev-frontend/src/theme.ts @@ -462,6 +462,16 @@ const theme: Theme = { "#root": { height: "100%" + }, + "html::-webkit-scrollbar": { + "display": "none" + }, + "html": { + /* this will hide the scrollbar in mozilla based browsers */ + "overflow": "-moz-scrollbars-none", + "scrollbar-width": "none", + /* this will hide the scrollbar in internet explorers */ + "-ms-overflow-style": "none" } }, From 64bb4f700ce50b5ac2e1758aaa44baa013b1f39a Mon Sep 17 00:00:00 2001 From: shmuel Date: Tue, 20 Jul 2021 11:46:36 +0300 Subject: [PATCH 43/90] refactor: + moving the bamm allowance to a seprate modal window --- .../src/components/Stability/NoDeposit.tsx | 17 ++++++------- .../Stability/StabilityDepositManager.tsx | 1 - packages/lib-base/etc/lib-base.api.md | 4 +-- packages/lib-base/src/StabilityDeposit.ts | 4 --- packages/lib-ethers/etc/lib-ethers.api.md | 6 +++++ .../lib-ethers/src/BlockPolledLiquityStore.ts | 12 ++++++--- packages/lib-ethers/src/EthersLiquity.ts | 4 +++ .../lib-ethers/src/ReadableEthersLiquity.ts | 25 +++++++++++++------ 8 files changed, 45 insertions(+), 28 deletions(-) diff --git a/packages/dev-frontend/src/components/Stability/NoDeposit.tsx b/packages/dev-frontend/src/components/Stability/NoDeposit.tsx index 145d1ce79..f7baf99a2 100644 --- a/packages/dev-frontend/src/components/Stability/NoDeposit.tsx +++ b/packages/dev-frontend/src/components/Stability/NoDeposit.tsx @@ -15,8 +15,8 @@ function useForceUpdate(){ return () => setValue(value => value + 1); // update the state to force render } -const selector = ({ stabilityDeposit }: LiquityStoreState) => ({ - stabilityDeposit +const selector = ( {bammAllowance}: any) => ({ + bammAllowance }); interface BammAllowanceModalProps { @@ -46,10 +46,9 @@ const BammAllowanceModal: React.FC = props => {
- - in order to deposit LUSD and gain LQTY & BPRO - you will need to unlock the BAMM smart contract - + + In order to depsoit LUSD to try B.AMM you need to grant allowance + @@ -61,7 +60,7 @@ const BammAllowanceModal: React.FC = props => { export const NoDeposit: React.FC = props => { const { liquity } = useLiquity(); - const { stabilityDeposit } = useLiquitySelector(selector); + const { bammAllowance } = useLiquitySelector(selector); const { dispatchEvent } = useStabilityView(); const [allowanceSucceed, setAllowanceSucceed] = useState(false); const [showModal, setShowModal] = useState(false); @@ -81,11 +80,11 @@ export const NoDeposit: React.FC = props => { } if (transactionState.type === "confirmed") { - setAllowanceSucceed(true); + handleOpenTrove() } }, [transactionState.type]); - const hasAllowance = allowanceSucceed || stabilityDeposit.bammAllowance + const hasAllowance = bammAllowance const modalProps = { close: ()=> setShowModal(false), diff --git a/packages/dev-frontend/src/components/Stability/StabilityDepositManager.tsx b/packages/dev-frontend/src/components/Stability/StabilityDepositManager.tsx index aea757956..80505bb88 100644 --- a/packages/dev-frontend/src/components/Stability/StabilityDepositManager.tsx +++ b/packages/dev-frontend/src/components/Stability/StabilityDepositManager.tsx @@ -97,7 +97,6 @@ export const StabilityDepositManager: React.FC = () => { const handleCancel = useCallback(() => { dispatchEvent("CANCEL_PRESSED"); }, [dispatchEvent]); - debugger const [validChange, description] = validateStabilityDepositChange( originalDeposit, editedUSD, diff --git a/packages/lib-base/etc/lib-base.api.md b/packages/lib-base/etc/lib-base.api.md index da8645009..15854ba81 100644 --- a/packages/lib-base/etc/lib-base.api.md +++ b/packages/lib-base/etc/lib-base.api.md @@ -601,10 +601,8 @@ export interface SentLiquityTransaction | undefined): Decimal; - // (undocumented) - readonly bammAllowance: boolean; readonly bammPoolShare: Decimal; readonly collateralGain: Decimal; readonly currentLUSD: Decimal; diff --git a/packages/lib-base/src/StabilityDeposit.ts b/packages/lib-base/src/StabilityDeposit.ts index 0f5fbdff2..26efaa4eb 100644 --- a/packages/lib-base/src/StabilityDeposit.ts +++ b/packages/lib-base/src/StabilityDeposit.ts @@ -45,8 +45,6 @@ export class StabilityDeposit { */ readonly frontendTag: string; - readonly bammAllowance: boolean; - readonly totalEthInBamm: Decimal; readonly totalLusdInBamm: Decimal; @@ -61,7 +59,6 @@ export class StabilityDeposit { collateralGain: Decimal, lqtyReward: Decimal, frontendTag: string, - bammAllowance: boolean, totalEthInBamm: Decimal, totalLusdInBamm: Decimal ) { @@ -73,7 +70,6 @@ export class StabilityDeposit { this.collateralGain = collateralGain; this.lqtyReward = lqtyReward; this.frontendTag = frontendTag; - this.bammAllowance = bammAllowance; this.totalEthInBamm = totalEthInBamm; this.totalLusdInBamm = totalLusdInBamm; } diff --git a/packages/lib-ethers/etc/lib-ethers.api.md b/packages/lib-ethers/etc/lib-ethers.api.md index 392b788b3..ad395e68f 100644 --- a/packages/lib-ethers/etc/lib-ethers.api.md +++ b/packages/lib-ethers/etc/lib-ethers.api.md @@ -60,6 +60,8 @@ export class BlockPolledLiquityStore extends LiquityStore; + // (undocumented) + getBammAllowance(overrides?: EthersCallOverrides): Promise; // @internal (undocumented) _getBlockTimestamp(blockTag?: BlockTag): Promise; // (undocumented) @@ -464,6 +468,8 @@ export class ReadableEthersLiquity implements ReadableLiquity { static _from(connection: EthersLiquityConnection): ReadableEthersLiquity; // @internal (undocumented) _getActivePool(overrides?: EthersCallOverrides): Promise; + // (undocumented) + getBammAllowance(overrides?: EthersCallOverrides): Promise; // @internal (undocumented) _getBlockTimestamp(blockTag?: BlockTag): Promise; // (undocumented) diff --git a/packages/lib-ethers/src/BlockPolledLiquityStore.ts b/packages/lib-ethers/src/BlockPolledLiquityStore.ts index 5a2029dac..9344d7bf7 100644 --- a/packages/lib-ethers/src/BlockPolledLiquityStore.ts +++ b/packages/lib-ethers/src/BlockPolledLiquityStore.ts @@ -38,6 +38,8 @@ export interface BlockPolledLiquityStoreExtraState { /** @internal */ _feesFactory: (blockTimestamp: number, recoveryMode: boolean) => Fees; + + bammAllowance: boolean; } /** @@ -92,6 +94,7 @@ export class BlockPolledLiquityStore extends LiquityStore { + return this._readable.getBammAllowance(overrides); + } + /** {@inheritDoc @liquity/lib-base#ReadableLiquity.getFrontendStatus} */ getFrontendStatus(address?: string, overrides?: EthersCallOverrides): Promise { return this._readable.getFrontendStatus(address, overrides); diff --git a/packages/lib-ethers/src/ReadableEthersLiquity.ts b/packages/lib-ethers/src/ReadableEthersLiquity.ts index cd5a7959a..51027e095 100644 --- a/packages/lib-ethers/src/ReadableEthersLiquity.ts +++ b/packages/lib-ethers/src/ReadableEthersLiquity.ts @@ -279,10 +279,9 @@ export class ReadableEthersLiquity implements ReadableLiquity { overrides?: EthersCallOverrides ): Promise { const _1e18 = BigNumber.from(10).pow(18) - const reallyLargeAllowance = BigNumber.from("0x8888888888888888888888888888888888888888888888888888888888888888") address ??= _requireAddress(this.connection); - const { stabilityPool, bamm, lqtyToken, lusdToken, priceFeed, bLens } = _getContracts(this.connection); + const { stabilityPool, bamm, lqtyToken, priceFeed, bLens } = _getContracts(this.connection); const bammLqtyBalancePromise = lqtyToken.balanceOf(bamm.address, { ...overrides}) const [ @@ -307,7 +306,6 @@ export class ReadableEthersLiquity implements ReadableLiquity { bLens.callStatic.getUnclaimedLqty(address, bamm.address, lqtyToken.address), bamm.share({ ...overrides }), bamm.stock({ ...overrides}), - ]); const bammLqtyBalance = await bammLqtyBalancePromise @@ -341,9 +339,6 @@ export class ReadableEthersLiquity implements ReadableLiquity { ) } - const allowance = await lusdToken.allowance(address, bamm.address) - console.log({allowance}) - const bammAllowance = allowance.gt(reallyLargeAllowance) return new StabilityDeposit( bammPoolShare, poolShare, @@ -353,7 +348,6 @@ export class ReadableEthersLiquity implements ReadableLiquity { Decimal.fromBigNumber(currentETH), Decimal.fromBigNumber(unclaimedLqty), frontEndTag, - bammAllowance, Decimal.fromBigNumber(bammEthBalance), Decimal.fromBigNumber(unclaimedLqty) ); @@ -618,6 +612,17 @@ export class ReadableEthersLiquity implements ReadableLiquity { ? { status: "registered", kickbackRate: decimalify(kickbackRate) } : { status: "unregistered" }; } + + async getBammAllowance(overrides?: EthersCallOverrides): Promise { + const { lusdToken, bamm } = _getContracts(this.connection); + const address = _requireAddress(this.connection); + const reallyLargeAllowance = BigNumber.from("0x8888888888888888888888888888888888888888888888888888888888888888") + + const allowance = await lusdToken.allowance(address, bamm.address) + console.log({allowance}) + const bammAllowance = allowance.gt(reallyLargeAllowance) + return bammAllowance; + } } type Resolved = T extends Promise ? U : T; @@ -872,4 +877,10 @@ class _BlockPolledReadableEthersLiquity _getRemainingLiquidityMiningLQTYRewardCalculator(): Promise<(blockTimestamp: number) => Decimal> { throw new Error("Method not implemented."); } + + async getBammAllowance(overrides?: EthersCallOverrides): Promise { + return this._blockHit(overrides) + ? this.store.state.bammAllowance + : this._readable.getBammAllowance(overrides); + } } From a01f8b1089196da31f0dc5974443d96775d4b068 Mon Sep 17 00:00:00 2001 From: shmuel Date: Tue, 20 Jul 2021 18:14:29 +0300 Subject: [PATCH 44/90] bugFix --- packages/lib-ethers/src/ReadableEthersLiquity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib-ethers/src/ReadableEthersLiquity.ts b/packages/lib-ethers/src/ReadableEthersLiquity.ts index 51027e095..10527ca8b 100644 --- a/packages/lib-ethers/src/ReadableEthersLiquity.ts +++ b/packages/lib-ethers/src/ReadableEthersLiquity.ts @@ -349,7 +349,7 @@ export class ReadableEthersLiquity implements ReadableLiquity { Decimal.fromBigNumber(unclaimedLqty), frontEndTag, Decimal.fromBigNumber(bammEthBalance), - Decimal.fromBigNumber(unclaimedLqty) + Decimal.fromBigNumber(currentBammLUSD) ); } From 8404868cdec861cff719cafc68c544672294d97a Mon Sep 17 00:00:00 2001 From: shmuel Date: Tue, 20 Jul 2021 18:15:02 +0300 Subject: [PATCH 45/90] improvment --- .../src/components/Stability/StabilityDepositEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dev-frontend/src/components/Stability/StabilityDepositEditor.tsx b/packages/dev-frontend/src/components/Stability/StabilityDepositEditor.tsx index 8fb6b9d5c..ed0fc3276 100644 --- a/packages/dev-frontend/src/components/Stability/StabilityDepositEditor.tsx +++ b/packages/dev-frontend/src/components/Stability/StabilityDepositEditor.tsx @@ -86,7 +86,7 @@ export const StabilityDepositEditor: React.FC = ({ /* LUSD balance ====================================================================*/ const newLusdBalance = editedBammPoolShare.mul(newTotalLusd).div(100) - let lusdDiff = Difference.between(newLusdBalance, stabilityDeposit.currentLUSD).nonZeroish(17) + let lusdDiff = Difference.between(newLusdBalance, stabilityDeposit.currentLUSD).nonZeroish(16) /* pool share From 68ed95ffda8b2d37b2de9903f97c9df6f37c499b Mon Sep 17 00:00:00 2001 From: shmuel Date: Wed, 21 Jul 2021 11:03:50 +0300 Subject: [PATCH 46/90] validatind network connection and allerting --- packages/dev-frontend/src/App.tsx | 4 ++-- packages/lib-ethers/src/EthersLiquityConnection.ts | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/dev-frontend/src/App.tsx b/packages/dev-frontend/src/App.tsx index 1253fe55d..2bb3fdeca 100644 --- a/packages/dev-frontend/src/App.tsx +++ b/packages/dev-frontend/src/App.tsx @@ -89,10 +89,10 @@ const App = () => { }} > - Liquity is not yet deployed to{" "} + B.Protocol AMM is not yet deployed to{" "} {chainId === 1 ? "mainnet" : "this network"}. - Please switch to Ropsten, Rinkeby, Kovan or Görli. + Please switch to Mainnet or Kovan.
); diff --git a/packages/lib-ethers/src/EthersLiquityConnection.ts b/packages/lib-ethers/src/EthersLiquityConnection.ts index d6cf89cf0..ed25d99d7 100644 --- a/packages/lib-ethers/src/EthersLiquityConnection.ts +++ b/packages/lib-ethers/src/EthersLiquityConnection.ts @@ -28,10 +28,11 @@ const deployments: { [chainId: number]: _LiquityDeploymentJSON | undefined; } = { [mainnet.chainId]: mainnet, - [ropsten.chainId]: ropsten, - [rinkeby.chainId]: rinkeby, - [goerli.chainId]: goerli, + //[ropsten.chainId]: ropsten, + //[rinkeby.chainId]: rinkeby, + //[goerli.chainId]: goerli, [kovan.chainId]: kovan, + [1337]: mainnet, ...(dev !== null ? { [dev.chainId]: dev } : {}) }; From 8d645826fdf7e273aaa01a645c8cb0eeef379292 Mon Sep 17 00:00:00 2001 From: shmuel Date: Wed, 21 Jul 2021 16:05:39 +0300 Subject: [PATCH 47/90] minor style change --- .../dev-frontend/src/components/Stability/NoDeposit.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/dev-frontend/src/components/Stability/NoDeposit.tsx b/packages/dev-frontend/src/components/Stability/NoDeposit.tsx index f7baf99a2..27f25bc7b 100644 --- a/packages/dev-frontend/src/components/Stability/NoDeposit.tsx +++ b/packages/dev-frontend/src/components/Stability/NoDeposit.tsx @@ -37,7 +37,7 @@ const BammAllowanceModal: React.FC = props => { overflow: "hidden", }}> - + Unlock BAMM @@ -45,7 +45,7 @@ const BammAllowanceModal: React.FC = props => { - + In order to depsoit LUSD to try B.AMM you need to grant allowance @@ -78,10 +78,6 @@ export const NoDeposit: React.FC = props => { if (transactionState.type === "waitingForConfirmation") { setShowModal(false); } - - if (transactionState.type === "confirmed") { - handleOpenTrove() - } }, [transactionState.type]); const hasAllowance = bammAllowance From 2e8cc4158b7a8cf69c281525b12d46bc09c0a19f Mon Sep 17 00:00:00 2001 From: shmuel Date: Thu, 22 Jul 2021 12:17:54 +0300 Subject: [PATCH 48/90] CF worker --- packages/dev-frontend/.gitignore | 5 +++++ packages/dev-frontend/package.json | 1 + yarn.lock | 9 ++++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/dev-frontend/.gitignore b/packages/dev-frontend/.gitignore index 70e9603ba..ed661a0cb 100644 --- a/packages/dev-frontend/.gitignore +++ b/packages/dev-frontend/.gitignore @@ -2,3 +2,8 @@ /build /.env /config.json + +# CF worker site +#=========================== +wrangler.toml +/workers-site \ No newline at end of file diff --git a/packages/dev-frontend/package.json b/packages/dev-frontend/package.json index f91ac0404..959cec609 100644 --- a/packages/dev-frontend/package.json +++ b/packages/dev-frontend/package.json @@ -4,6 +4,7 @@ "private": true, "homepage": ".", "dependencies": { + "@cloudflare/kv-asset-handler": "^0.1.0", "@ethersproject/abi": "5.3.1", "@fortawesome/fontawesome-svg-core": "1.2.34", "@fortawesome/free-regular-svg-icons": "5.15.2", diff --git a/yarn.lock b/yarn.lock index 5736ad9f4..ba6c2baf0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1238,6 +1238,13 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@cloudflare/kv-asset-handler@^0.1.0": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.1.3.tgz#3eaaf962b16c48a7189db5d5ac1c4dca4e2ed1a0" + integrity sha512-FNcunDuTmEfQTLRLtA6zz+buIXUHj1soPvSWzzQFBC+n2lsy+CGf/NIrR3SEPCmsVNQj70/Jx2lViCpq+09YpQ== + dependencies: + mime "^2.5.2" + "@cnakazawa/watch@^1.0.3": version "1.0.4" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" @@ -14148,7 +14155,7 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.4.3, mime@^2.4.4: +mime@^2.4.3, mime@^2.4.4, mime@^2.5.2: version "2.5.2" resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== From 64d950946bb468a6b7fbb68795e3ddde20c7066f Mon Sep 17 00:00:00 2001 From: shmuel Date: Thu, 22 Jul 2021 13:00:26 +0300 Subject: [PATCH 49/90] adding b.protocol icon --- packages/dev-frontend/public/favicon.ico | 1 + packages/dev-frontend/public/favicon.png | Bin 1201 -> 0 bytes packages/dev-frontend/public/favicon.svg | 1 + packages/dev-frontend/public/index.html | 2 +- packages/dev-frontend/public/manifest.json | 2 +- 5 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 packages/dev-frontend/public/favicon.ico delete mode 100644 packages/dev-frontend/public/favicon.png create mode 100644 packages/dev-frontend/public/favicon.svg diff --git a/packages/dev-frontend/public/favicon.ico b/packages/dev-frontend/public/favicon.ico new file mode 100644 index 000000000..65cb2c890 --- /dev/null +++ b/packages/dev-frontend/public/favicon.ico @@ -0,0 +1 @@ +F40D6CE7-4B76-44BF-B583-679B75EAE8C5Created with sketchtool. \ No newline at end of file diff --git a/packages/dev-frontend/public/favicon.png b/packages/dev-frontend/public/favicon.png deleted file mode 100644 index 8855560a79331c795dc4470ccb7b02cff6f9942a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1201 zcmV;i1Wx;jP)Hh0LkL5CJ>0(7_=j_<dI#8bv;7|kfa(wftOJHXFwFKD zK|D}^yi*LE$iPWGHU1Ys>t1{0Qge-)v7tKEi6r6Z$L~hbYC@{jJuL&f?h* z-ngr8hk@@2v(4%r)BsmnToZ?Cm=-JN>i>UEZH|UMOSBr;ztt0Bd&z%P0Oa)Jeou*{ z_ZG4fE6ZZev(L`#Kd|F?v9pWk7GrubL9#>2L38i7>^v0!3A+BLodv~eVni6b7Fv5j zb$a4;*I0|+sr;5C9J@NW8lB`)&lxI);X+TC4Nw7`f5I{LT76~S8QE0tbb-qUB1?-| zVX!}b^q&lNnq3!BAsAv^Q~)#z5NErpkYMFl3xzS9qyngBkTCA|l$>aDL*}o1nMBin zWXkR(O3B91M+HEP&9Z=i-281L{rgYrC4p#>HKWu3M1_$y_i`nJ8jLn2)$r^7#0sq< zO4BN*8xn|5k2(2g+&hf&TVab#ZgSzv2_H?(C<{Q{(6`slzjwrueYCQ|FwKh7)VLcC zEzbi&kJHc=+dNifi(WeBoNuURO70$GEOfsUO&d0+`%x&M#^E@J08Aydf+HZ`x9rIj z#|MQ9qXb|80i3pSLdvoKk)1OQ)l9Z~CS6dJ$XdNr{d;seCC!A8I5lj6d%2YW!e!j7DNWsBjVWX8p!?r((2AnKE!@Y#IybzEBjL-Ryrhc z{@rocOl?tC{P0zBYLZLqDu6Kz2E%N-0&w{HTD=|T?ii_1JOSSiU@Xj~q`%1bgN}-N z?Rchk#Pc0@^~W;+5OcsP?O1i%G#n=b!dX3tCMPo+dfsI@32E0_oW3s@X1j=e9)QGx zl*w^MkCZnLJ~ZChP(@Yi&7vP;h)4|h)VVMZKxmE(#k~vy4ghqcoK(PIbIrrrNY|d) zau6YfM@ByFWLyA113_#~)$N*m|4B!k7Gygs&wY4yai8HlpyRyY3bZFtlmL9qtVEkV z&5GlQ$ZnINUGL3UqJX;izxog`3QSr6l5P?MT}E$2LjV=zsp|c0O13U^uXqVDgd P00000NkvXXu0mjfb^A2k diff --git a/packages/dev-frontend/public/favicon.svg b/packages/dev-frontend/public/favicon.svg new file mode 100644 index 000000000..65cb2c890 --- /dev/null +++ b/packages/dev-frontend/public/favicon.svg @@ -0,0 +1 @@ +F40D6CE7-4B76-44BF-B583-679B75EAE8C5Created with sketchtool. \ No newline at end of file diff --git a/packages/dev-frontend/public/index.html b/packages/dev-frontend/public/index.html index 0e8e4800a..e4c7d2caf 100644 --- a/packages/dev-frontend/public/index.html +++ b/packages/dev-frontend/public/index.html @@ -2,7 +2,7 @@ - + Date: Thu, 22 Jul 2021 13:01:00 +0300 Subject: [PATCH 50/90] adding a link to the terms --- packages/dev-frontend/src/components/WalletConnector.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/dev-frontend/src/components/WalletConnector.tsx b/packages/dev-frontend/src/components/WalletConnector.tsx index 5e706f41a..4bc6fbbc9 100644 --- a/packages/dev-frontend/src/components/WalletConnector.tsx +++ b/packages/dev-frontend/src/components/WalletConnector.tsx @@ -117,7 +117,7 @@ export const WalletConnector: React.FC = ({ children, load return ( <> - + + + By using bprotocol, you agree to the Terms and Conditions + {connectionState.type === "failed" && ( From c572ff588cd15a3bd23660a96e9de4cea52fe9ed Mon Sep 17 00:00:00 2001 From: shmuel Date: Sun, 25 Jul 2021 11:40:19 +0300 Subject: [PATCH 51/90] hide: + nav links + mobile nav --- .../dev-frontend/src/components/Header.tsx | 9 +- packages/dev-frontend/src/components/Nav.tsx | 8 +- packages/dev-frontend/src/index.css | 4 + yarn.lock | 271 +++++++++++------- 4 files changed, 175 insertions(+), 117 deletions(-) diff --git a/packages/dev-frontend/src/components/Header.tsx b/packages/dev-frontend/src/components/Header.tsx index 512d9d0fe..93e6aea76 100644 --- a/packages/dev-frontend/src/components/Header.tsx +++ b/packages/dev-frontend/src/components/Header.tsx @@ -25,8 +25,10 @@ export const Header: React.FC = ({ children }) => { return ( +
- +
+
{ borderLeft: ["none", "1px solid lightgrey"] }} /> +
{isFrontendRegistered && ( <> - +
+ +