diff --git a/package.json b/package.json index d2dcec6b7..d4fdaa1e0 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ ] }, "scripts": { + "cf-build": "yarn && run-s build:dev-frontend", "build": "run-s build:*", "build:dev-frontend": "yarn workspace @liquity/dev-frontend build", "build:subgraph": "yarn workspace @liquity/subgraph build", @@ -53,10 +54,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/contracts/contracts/B.Protocol/BAMM.sol b/packages/contracts/contracts/B.Protocol/BAMM.sol new file mode 100644 index 000000000..ee6ee65b6 --- /dev/null +++ b/packages/contracts/contracts/B.Protocol/BAMM.sol @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.11; + +import "./../StabilityPool.sol"; +import "./CropJoinAdapter.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 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% + 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 immutable frontEndTag; + + uint constant public 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, + address _LUSD, + address _LQTY, + uint _maxDiscount, + address payable _feePool, + address _fronEndTag) + public + CropJoinAdapter(_LQTY) + { + priceAggregator = AggregatorV3Interface(_priceAggregator); + LUSD = IERC20(_LUSD); + SP = StabilityPool(_SP); + + feePool = _feePool; + maxDiscount = _maxDiscount; + frontEndTag = _fronEndTag; + } + + 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; + + emit ParamsSet(_A, _fee); + } + + 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); + + // 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(total > 0) newShare = total.mul(lusdAmount) / totalValue; + + // deposit + require(LUSD.transferFrom(msg.sender, address(this), lusdAmount), "deposit: transferFrom failed"); + SP.provideToSP(lusdAmount, frontEndTag); + + // update LP token + mint(msg.sender, newShare); + + emit UserDeposit(msg.sender, lusdAmount, 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 LP token + burn(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"); + } + + emit UserWithdraw(msg.sender, lusdAmount, ethAmount, numShares); + } + + 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, uint minEthReturn, address payable dest) public 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); + + if(feeAmount > 0) feePool.transfer(feeAmount); + (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; + } + + // 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, 0, 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 {} +} 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/contracts/B.Protocol/ChainlinkTestnet.sol b/packages/contracts/contracts/B.Protocol/ChainlinkTestnet.sol new file mode 100644 index 000000000..c956fe1f3 --- /dev/null +++ b/packages/contracts/contracts/B.Protocol/ChainlinkTestnet.sol @@ -0,0 +1,41 @@ +// 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; + uint time = 0; + + constructor(PriceFeedTestnet _feed) public { + feed = _feed; + } + + function decimals() external pure returns(uint) { + return 18; + } + + function setTimestamp(uint _time) external { + time = _time; + } + + function latestRoundData() external view returns + ( + uint80 /* roundId */, + int256 answer, + uint256 /* startedAt */, + uint256 timestamp, + uint80 /* answeredInRound */ + ) + { + answer = int(feed.getPrice()); + if(time == 0 ) timestamp = now; + else timestamp = time; + } +} diff --git a/packages/contracts/contracts/B.Protocol/CropJoinAdapter.sol b/packages/contracts/contracts/B.Protocol/CropJoinAdapter.sol new file mode 100644 index 000000000..f2f982f74 --- /dev/null +++ b/packages/contracts/contracts/B.Protocol/CropJoinAdapter.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.11; + +import "./crop.sol"; +import "./../StabilityPool.sol"; + +// 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; + + event Transfer(address indexed _from, address indexed _to, uint256 _value); + + constructor(address _lqty) public + CropJoin(address(new Dummy()), "B.AMM", address(new DummyGem()), _lqty) + { + } + + // adapter to cropjoin + function nav() public override returns (uint256) { + return total; + } + + function totalSupply() public view returns (uint256) { + return total; + } + + function balanceOf(address owner) public view returns (uint256 balance) { + balance = stake[owner]; + } + + 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); + } +} + +contract Dummy { + fallback() external {} +} + +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/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..50d2c371f --- /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 public 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"); + } + + // 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); + } + + function burn(address owner, uint value) override internal { + pickleJar.depositAll(); + super.burn(owner, value); + } +} diff --git a/packages/contracts/contracts/B.Protocol/PriceFormula.sol b/packages/contracts/contracts/B.Protocol/PriceFormula.sol new file mode 100644 index 000000000..a0545a318 --- /dev/null +++ b/packages/contracts/contracts/B.Protocol/PriceFormula.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT + +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 <= sum.add(1)) 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 <= y.add(1)) 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..8eb279ef0 --- /dev/null +++ b/packages/contracts/contracts/B.Protocol/crop.sol @@ -0,0 +1,167 @@ +// 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) { + uint256 _nav = gem.balanceOf(address(this)); + return mul(_nav, to18ConversionFactor); + } + + // 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); + } +} 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. diff --git a/packages/contracts/test/B.Protocol/BAMMTest.js b/packages/contracts/test/B.Protocol/BAMMTest.js new file mode 100644 index 000000000..3674ca5e9 --- /dev/null +++ b/packages/contracts/test/B.Protocol/BAMMTest.js @@ -0,0 +1,801 @@ +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 BLens = artifacts.require("BLens.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('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, + 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 lens + 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("BAMM", 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 }) + + bamm = await BAMM.new(chainlink.address, stabilityPool.address, lusdToken.address, lqtyToken.address, 400, feePool, frontEnd_1, {from: bammOwner}) + lens = await BLens.new() + }) + + // --- 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 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) + + 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 }) + + // 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((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()) + }) + + 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 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, 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 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 } }) + + // 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), 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 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('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(105,18), {from: whale}) + const dest = "0xdEADBEEF00AA81bBCF694bC5c05A397F5E5658D5" + + 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(6105, 18)).toString(), (await stabilityPool.getCompoundedLUSDDeposit(bamm.address)).toString()) + + // check eth balance + assert.equal(await web3.eth.getBalance(dest), priceWithFee.ethAmount) + + // check fees + assert.equal(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.skip('transfer happy test', async () => { // transfer is not supported anymore + // --- 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)) + }) + + + // 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 - transfer sad test + // 11. pickle V + // 10. cleanups - compilation warnings. cropjoin - revoke changes and maybe make internal. V + // 12 - linter. events + }) +}) + + +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 diff --git a/packages/contracts/test/B.Protocol/PickleTest.js b/packages/contracts/test/B.Protocol/PickleTest.js new file mode 100644 index 000000000..81893eb34 --- /dev/null +++ b/packages/contracts/test/B.Protocol/PickleTest.js @@ -0,0 +1,810 @@ +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 BLens = artifacts.require("BLens.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}) + lens = await BLens.new() + }) + + // --- 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) + + 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 }) + + // 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.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()) + }) + + 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(105,18), {from: whale}) + const dest = "0xdEADBEEF00AA81bBCF694bC5c05A397F5E5658D5" + await bamm.swap(dec(105,18), priceWithFee.ethAmount, dest, {from: whale}) + + // check lusd balance + assert.equal(toBN(dec(6105, 18)).toString(), (await stabilityPool.getCompoundedLUSDDeposit(bamm.address)).toString()) + + // check eth balance + assert.equal(await web3.eth.getBalance(dest), priceWithFee.ethAmount) + + // check fees + assert.equal(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.skip('transfer happy test', async () => { // transfer is not supported anymore + // --- 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 diff --git a/packages/contracts/test/B.Protocol/PriceFormulaTest.js b/packages/contracts/test/B.Protocol/PriceFormulaTest.js new file mode 100644 index 000000000..9df5959ba --- /dev/null +++ b/packages/contracts/test/B.Protocol/PriceFormulaTest.js @@ -0,0 +1,88 @@ +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') + }) + + 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 +} + 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 4cf87fed1..1205bfe41 100644 --- a/packages/dev-frontend/package.json +++ b/packages/dev-frontend/package.json @@ -47,7 +47,9 @@ "start-demo": "cross-env REACT_APP_DEMO_MODE=true run-s start", "build": "run-s build:*", "build:set-version": "node scripts/set-version.js", + "build:remove-iframe-deny": "node scripts/remove-iframe-deny.js", "build:react": "react-scripts build", + "build:static": "./scripts/generate-static.sh", "test": "react-scripts test", "eject": "react-scripts eject" }, @@ -65,5 +67,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "@cloudflare/kv-asset-handler": "^0.1.3" } } diff --git a/packages/dev-frontend/public/bprotocol/group-1.png b/packages/dev-frontend/public/bprotocol/group-1.png new file mode 100644 index 000000000..050b09ab9 Binary files /dev/null and b/packages/dev-frontend/public/bprotocol/group-1.png differ diff --git a/packages/dev-frontend/public/bprotocol/group-2.png b/packages/dev-frontend/public/bprotocol/group-2.png new file mode 100644 index 000000000..45aa499c2 Binary files /dev/null and b/packages/dev-frontend/public/bprotocol/group-2.png differ diff --git a/packages/dev-frontend/public/bprotocol/group-3.png b/packages/dev-frontend/public/bprotocol/group-3.png new file mode 100644 index 000000000..ae401974e Binary files /dev/null and b/packages/dev-frontend/public/bprotocol/group-3.png differ diff --git a/packages/dev-frontend/public/bprotocol/group.png b/packages/dev-frontend/public/bprotocol/group.png new file mode 100644 index 000000000..6918ce152 Binary files /dev/null and b/packages/dev-frontend/public/bprotocol/group.png differ diff --git a/packages/dev-frontend/public/bprotocol/icon-a-1.svg b/packages/dev-frontend/public/bprotocol/icon-a-1.svg new file mode 100644 index 000000000..aad64d9a3 --- /dev/null +++ b/packages/dev-frontend/public/bprotocol/icon-a-1.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/dev-frontend/public/bprotocol/icon-a-2.svg b/packages/dev-frontend/public/bprotocol/icon-a-2.svg new file mode 100644 index 000000000..eb8238c61 --- /dev/null +++ b/packages/dev-frontend/public/bprotocol/icon-a-2.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/dev-frontend/public/bprotocol/icon-a-3.svg b/packages/dev-frontend/public/bprotocol/icon-a-3.svg new file mode 100644 index 000000000..228ab5631 --- /dev/null +++ b/packages/dev-frontend/public/bprotocol/icon-a-3.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + 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 8855560a7..000000000 Binary files a/packages/dev-frontend/public/favicon.png and /dev/null differ 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/fonts/NeueHaasGroteskDisp Pro Md.eot b/packages/dev-frontend/public/fonts/NeueHaasGroteskDisp Pro Md.eot new file mode 100644 index 000000000..663c783dc Binary files /dev/null and b/packages/dev-frontend/public/fonts/NeueHaasGroteskDisp Pro Md.eot differ diff --git a/packages/dev-frontend/public/fonts/NeueHaasGroteskDisp Pro Md.ttf b/packages/dev-frontend/public/fonts/NeueHaasGroteskDisp Pro Md.ttf new file mode 100644 index 000000000..856e4f226 Binary files /dev/null and b/packages/dev-frontend/public/fonts/NeueHaasGroteskDisp Pro Md.ttf differ diff --git a/packages/dev-frontend/public/fonts/NeueHaasGroteskDisp Pro Md.woff b/packages/dev-frontend/public/fonts/NeueHaasGroteskDisp Pro Md.woff new file mode 100644 index 000000000..9de7cb556 Binary files /dev/null and b/packages/dev-frontend/public/fonts/NeueHaasGroteskDisp Pro Md.woff differ diff --git a/packages/dev-frontend/public/fonts/NeueHaasGroteskDisp Pro Md.woff2 b/packages/dev-frontend/public/fonts/NeueHaasGroteskDisp Pro Md.woff2 new file mode 100644 index 000000000..d9182cb71 Binary files /dev/null and b/packages/dev-frontend/public/fonts/NeueHaasGroteskDisp Pro Md.woff2 differ diff --git a/packages/dev-frontend/public/index.html b/packages/dev-frontend/public/index.html index 0e8e4800a..6616897bf 100644 --- a/packages/dev-frontend/public/index.html +++ b/packages/dev-frontend/public/index.html @@ -2,7 +2,19 @@ - + + + { }} > - 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 Kovan. ); diff --git a/packages/dev-frontend/src/components/ConnectPage.tsx b/packages/dev-frontend/src/components/ConnectPage.tsx new file mode 100644 index 000000000..160d77518 --- /dev/null +++ b/packages/dev-frontend/src/components/ConnectPage.tsx @@ -0,0 +1,247 @@ +import React from "react"; +import { Flex, Link, Box } from "theme-ui"; + +type ConnectPageProps = { +}; + +type ItemProps = { + icon: string, + title: React.ReactElement, + text: string, + link: any +}; + +export const device = { + mobile: 300, + tablet: 700, + laptop: 1000, + desktop: 1200 +} + +const Item: React.FC = ({icon, title, text, link}) => { + return ( + + + + + + {title} + + + {text} + {" "} + here. + + + ) +} + + + +export const ConnectPage: React.FC = ({children}) => { + const GROUP = "./bprotocol/group.png" + const GROUP_1 = "./bprotocol/group-1.png" + const GROUP_2 = "./bprotocol/group-2.png" + const GROUP_3 = "./bprotocol/group-3.png" + return ( +
+ + + Automated Rebalancing for Liquity Stability Pool + + + Powered by B.Protocol v2 + + + + {children} + + + Stabilize
Liquity Protocol} + text="B.Protocol v2 and its novel Backstop AMM (B.AMM) automates the rebalancing of Liquity Stability Pool to maintain its strength. + Read more on how the Liquity + SP is working " + link="https://docs.liquity.org/faq/stability-pool-and-liquidations" + /> + Get Passive
+ Yield on Your LUSD} + text="By using B.Protocol to deposit your LUSD into Liquity Stability Pool, you can save the manual operation of selling your accumulated ETH back to LUSD every time a liquidation is taking place. + Read more about how it’s done" + link="https://medium.com/b-protocol/b-protocol-liquity-integration-is-live-1342605e7cfb" + /> + Using
+ B.Protocl V2} + text="The integration of Liqity with B.Protocol v2 is a step forward towards a more stabilized DeFi ecosystem. + Read more about the novel B.AMM design that enables that" + link="https://medium.com/b-protocol/b-amm-efficient-automated-market-maker-for-defi-liquidations-fea7b0fdc0c5" + /> +
+
+ ) +} \ No newline at end of file diff --git a/packages/dev-frontend/src/components/Header.tsx b/packages/dev-frontend/src/components/Header.tsx index 512d9d0fe..8c41528e2 100644 --- a/packages/dev-frontend/src/components/Header.tsx +++ b/packages/dev-frontend/src/components/Header.tsx @@ -26,7 +26,7 @@ export const Header: React.FC = ({ children }) => { - +
{ borderLeft: ["none", "1px solid lightgrey"] }} /> +
{isFrontendRegistered && ( <> - +
+ +