From 48a07b6960461068fb301b814b65047384d066fe Mon Sep 17 00:00:00 2001 From: bghughes Date: Mon, 16 May 2022 16:09:32 -0500 Subject: [PATCH 1/2] Rubicon v1.1.0 --- contracts/.DS_Store | Bin 6148 -> 6148 bytes contracts/RubiconMarket.sol | 16 +- contracts/RubiconRouter.sol | 36 +- contracts/interfaces/IBathHouse.sol | 5 +- contracts/interfaces/IBathToken.sol | 6 + contracts/interfaces/IVestingWallet.sol | 14 + contracts/libraries/ABDKMath64x64.sol | 805 --------------- contracts/libraries/TransferHelper.sol | 79 -- .../peripheral_contracts/VestingWallet.sol | 158 +++ contracts/rubiconPools/BathHouse.sol | 272 +++-- contracts/rubiconPools/BathPair.sol | 284 +++--- contracts/rubiconPools/BathToken.sol | 458 ++++++++- test/BathHouse_test.js | 42 - test/BathPair_test.js | 50 - test/BathToken_test.js | 60 -- test/RubiconMarket_test.js | 42 - test/RubiconRouter_test.js | 81 +- test/oldTests/1_protocol_test.js | 954 ++++++++---------- test/oldTests/2_router_test.js | 15 +- 19 files changed, 1426 insertions(+), 1951 deletions(-) create mode 100644 contracts/interfaces/IVestingWallet.sol delete mode 100644 contracts/libraries/ABDKMath64x64.sol delete mode 100644 contracts/libraries/TransferHelper.sol create mode 100644 contracts/peripheral_contracts/VestingWallet.sol delete mode 100644 test/RubiconMarket_test.js diff --git a/contracts/.DS_Store b/contracts/.DS_Store index aba5801b8cd68ff7be2fbd755451f7a8a1737e07..40c93a6138dfb12dfb9e7a698af6683c7a1ac320 100644 GIT binary patch delta 30 mcmZoMXfc@J&&WJ6U^gT4WFAJ9&Hju#*d{i}Z)WHC%MSpMX$fxt delta 43 xcmZoMXfc@J&&V<{U^gSnWFAHpeolrQhD?Seh9ZVUAkN&Zz<7dfGdss$egN_83m*Ug diff --git a/contracts/RubiconMarket.sol b/contracts/RubiconMarket.sol index 970df3a..022720b 100644 --- a/contracts/RubiconMarket.sol +++ b/contracts/RubiconMarket.sol @@ -2,7 +2,7 @@ /// This contract is a derivative work of the open-source work of Oasis DEX: https://github.com/OasisDEX/oasis /// @title RubiconMarket.sol -/// @notice Please see the repository for this code at https://github.com/RubiconDeFi/rubicon_protocol; +/// @notice Please see the repository for this code at https://github.com/RubiconDeFi/rubicon-protocol-v1; pragma solidity =0.7.6; @@ -292,6 +292,7 @@ contract SimpleMarket is EventfulMarket, DSMath { return false; } + // Fee logic added on taker trades uint256 fee = mul(spend, feeBPS) / 10000; require( _offer.buy_gem.transferFrom(msg.sender, feeTo, fee), @@ -528,7 +529,9 @@ contract RubiconMarket is MatchingEvents, ExpiringMarket, DSNote { /// @dev Below is variable to allow for a proxy-friendly constructor bool public initialized; + /// @dev unused deprecated variable for applying a token distribution on top of a trade bool public AqueductDistributionLive; + /// @dev unused deprecated variable for applying a token distribution of this token on top of a trade address public AqueductAddress; struct sortInfo { @@ -546,9 +549,10 @@ contract RubiconMarket is MatchingEvents, ExpiringMarket, DSNote { /// @dev Proxy-safe initialization of storage function initialize(bool _live, address _feeTo) public { - // require(msg.sender == ___deployer____); require(!initialized, "contract is already initialized"); AqueductDistributionLive = _live; + + /// @notice The market fee recipient feeTo = _feeTo; owner = msg.sender; @@ -556,6 +560,7 @@ contract RubiconMarket is MatchingEvents, ExpiringMarket, DSNote { /// @notice The starting fee on taker trades in basis points feeBPS = 20; + initialized = true; matchingEnabled = true; buyEnabled = true; @@ -612,8 +617,9 @@ contract RubiconMarket is MatchingEvents, ExpiringMarket, DSNote { ) public override returns (uint256) { require(!locked, "Reentrancy attempt"); - function(uint256, ERC20, uint256, ERC20) - returns (uint256) fn = matchingEnabled ? _offeru : super.offer; + + function(uint256, ERC20, uint256, ERC20) returns (uint256) fn + = matchingEnabled ? _offeru : super.offer; return fn(pay_amt, pay_gem, buy_amt, buy_gem); } @@ -1227,6 +1233,7 @@ contract RubiconMarket is MatchingEvents, ExpiringMarket, DSNote { return true; } + /// @dev unused deprecated function for applying a token distribution on top of a trade function setAqueductDistributionLive(bool live) external auth @@ -1236,6 +1243,7 @@ contract RubiconMarket is MatchingEvents, ExpiringMarket, DSNote { return true; } + /// @dev unused deprecated variable for applying a token distribution on top of a trade function setAqueductAddress(address _Aqueduct) external auth diff --git a/contracts/RubiconRouter.sol b/contracts/RubiconRouter.sol index 792f02c..06c59ce 100644 --- a/contracts/RubiconRouter.sol +++ b/contracts/RubiconRouter.sol @@ -8,10 +8,8 @@ pragma abicoder v2; import "./RubiconMarket.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/math/SafeMath.sol"; -import "./libraries/ABDKMath64x64.sol"; import "./peripheral_contracts/WETH9.sol"; // @unsupported: ovm -import "./rubiconPools/BathToken.sol"; -import "./libraries/TransferHelper.sol"; +import "./interfaces/IBathToken.sol"; ///@dev this contract is a high-level router that utilizes Rubicon smart contracts to provide ///@dev added convenience and functionality when interacting with the Rubicon protocol @@ -106,9 +104,9 @@ contract RubiconRouter { continue; } uint256 nextBestAsk = RubiconMarket(_RubiconMarketAddress) - .getWorseOffer(lastAsk); + .getWorseOffer(lastAsk); uint256 nextBestBid = RubiconMarket(_RubiconMarketAddress) - .getWorseOffer(lastBid); + .getWorseOffer(lastBid); (uint256 ask_pay_amt, , uint256 ask_buy_amt, ) = RubiconMarket( _RubiconMarketAddress ).getOffer(nextBestAsk); @@ -330,6 +328,8 @@ contract RubiconRouter { uint256 max_fill_amount, uint256 expectedMarketFeeBPS ) external payable returns (uint256 fill) { + address _weth = address(wethAddress); + uint256 _before = ERC20(_weth).balanceOf(address(this)); uint256 max_fill_withFee = max_fill_amount.add( max_fill_amount.mul(expectedMarketFeeBPS).div(10000) ); @@ -345,13 +345,16 @@ contract RubiconRouter { ERC20(wethAddress), max_fill_amount ); - if (max_fill_amount > fill) { - emit LogNote("returned fill", fill); + ERC20(buy_gem).transfer(msg.sender, buy_amt); + + uint256 _after = ERC20(_weth).balanceOf(address(this)); + uint256 delta = _after - _before; - // ERC20(pay_gem).transfer(msg.sender, max_fill_amount - fill); + // Return unspent coins to sender + if (delta > 0) { + WETH9(wethAddress).withdraw(delta); + msg.sender.transfer(delta); } - ERC20(buy_gem).transfer(msg.sender, buy_amt); - return fill; } // Paying ERC20 to buy native ETH @@ -454,7 +457,7 @@ contract RubiconRouter { payable returns (uint256 newShares) { - IERC20 target = BathToken(targetPool).underlyingToken(); + IERC20 target = IBathToken(targetPool).underlyingToken(); require(target == ERC20(wethAddress), "target pool not weth pool"); require(msg.value >= amount, "didnt send enough eth"); @@ -463,7 +466,7 @@ contract RubiconRouter { } WETH9(wethAddress).deposit{value: amount}(); - newShares = BathToken(targetPool).deposit(amount); + newShares = IBathToken(targetPool).deposit(amount); //Send back bathTokens to sender ERC20(targetPool).transfer(msg.sender, newShares); } @@ -474,15 +477,16 @@ contract RubiconRouter { payable returns (uint256 withdrawnWETH) { - IERC20 target = BathToken(targetPool).underlyingToken(); + IERC20 target = IBathToken(targetPool).underlyingToken(); require(target == ERC20(wethAddress), "target pool not weth pool"); require( - BathToken(targetPool).balanceOf(msg.sender) >= shares, + IBathToken(targetPool).balanceOf(msg.sender) >= shares, "don't own enough shares" ); - BathToken(targetPool).transferFrom(msg.sender, address(this), shares); - withdrawnWETH = BathToken(targetPool).withdraw(shares); + IBathToken(targetPool).transferFrom(msg.sender, address(this), shares); + withdrawnWETH = IBathToken(targetPool).withdraw(shares); WETH9(wethAddress).withdraw(withdrawnWETH); + //Send back withdrawn native eth to sender msg.sender.transfer(withdrawnWETH); } diff --git a/contracts/interfaces/IBathHouse.sol b/contracts/interfaces/IBathHouse.sol index ca90055..f6827ec 100644 --- a/contracts/interfaces/IBathHouse.sol +++ b/contracts/interfaces/IBathHouse.sol @@ -7,10 +7,11 @@ interface IBathHouse { function initialized() external returns (bool); - function reserveRatio() external view returns (uint); + function reserveRatio() external view returns (uint256); function tokenToBathToken(address erc20Address) - external view + external + view returns (address bathTokenAddress); function isApprovedStrategist(address wouldBeStrategist) diff --git a/contracts/interfaces/IBathToken.sol b/contracts/interfaces/IBathToken.sol index 79e7079..fe8f9d8 100644 --- a/contracts/interfaces/IBathToken.sol +++ b/contracts/interfaces/IBathToken.sol @@ -41,6 +41,8 @@ interface IBathToken is IERC20 { function setMarket(address newRubiconMarket) external; + function setBonusToken(address newBonusToken) external; + function setFeeBPS(uint256 _feeBPS) external; function setFeeTo(address _feeTo) external; @@ -53,6 +55,10 @@ interface IBathToken is IERC20 { function deposit(uint256 amount) external returns (uint256 shares); + function deposit(uint256 assets, address receiver) + external + returns (uint256 shares); + function withdraw(uint256 shares) external returns (uint256 amount); function DOMAIN_SEPARATOR() external view returns (bytes32); diff --git a/contracts/interfaces/IVestingWallet.sol b/contracts/interfaces/IVestingWallet.sol new file mode 100644 index 0000000..3a88301 --- /dev/null +++ b/contracts/interfaces/IVestingWallet.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.7.6; + +interface IVestingWallet { + function beneficiary() external view returns (address); + + function release(address token) external; + + function vestedAmount(address token, uint64 timestamp) + external + view + returns (uint256); +} diff --git a/contracts/libraries/ABDKMath64x64.sol b/contracts/libraries/ABDKMath64x64.sol deleted file mode 100644 index 8974974..0000000 --- a/contracts/libraries/ABDKMath64x64.sol +++ /dev/null @@ -1,805 +0,0 @@ -// SPDX-License-Identifier: BSD-4-Clause -/* - * ABDK Math 64.64 Smart Contract Library. Copyright © 2019 by ABDK Consulting. - * Author: Mikhail Vladimirov - */ -pragma solidity =0.7.6; //<0.6.0-0||>=0.6.0 <0.7.0-0||>=0.7.0 <0.8.0-0; - -/** - * Smart contract library of mathematical functions operating with signed - * 64.64-bit fixed point numbers. Signed 64.64-bit fixed point number is - * basically a simple fraction whose numerator is signed 128-bit integer and - * denominator is 2^64. As long as denominator is always the same, there is no - * need to store it, thus in Solidity signed 64.64-bit fixed point numbers are - * represented by int128 type holding only the numerator. - */ -library ABDKMath64x64 { - /* - * Minimum value signed 64.64-bit fixed point number may have. - */ - int128 private constant MIN_64x64 = -0x80000000000000000000000000000000; - - /* - * Maximum value signed 64.64-bit fixed point number may have. - */ - int128 private constant MAX_64x64 = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - - /** - * Convert signed 256-bit integer number into signed 64.64-bit fixed point - * number. Revert on overflow. - * - * @param x signed 256-bit integer number - * @return signed 64.64-bit fixed point number - */ - function fromInt(int256 x) internal pure returns (int128) { - require(x >= -0x8000000000000000 && x <= 0x7FFFFFFFFFFFFFFF); - return int128(x << 64); - } - - /** - * Convert signed 64.64 fixed point number into signed 64-bit integer number - * rounding down. - * - * @param x signed 64.64-bit fixed point number - * @return signed 64-bit integer number - */ - function toInt(int128 x) internal pure returns (int64) { - return int64(x >> 64); - } - - /** - * Convert unsigned 256-bit integer number into signed 64.64-bit fixed point - * number. Revert on overflow. - * - * @param x unsigned 256-bit integer number - * @return signed 64.64-bit fixed point number - */ - function fromUInt(uint256 x) internal pure returns (int128) { - require(x <= 0x7FFFFFFFFFFFFFFF); - return int128(x << 64); - } - - /** - * Convert signed 64.64 fixed point number into unsigned 64-bit integer - * number rounding down. Revert on underflow. - * - * @param x signed 64.64-bit fixed point number - * @return unsigned 64-bit integer number - */ - function toUInt(int128 x) internal pure returns (uint64) { - require(x >= 0); - return uint64(x >> 64); - } - - /** - * Convert signed 128.128 fixed point number into signed 64.64-bit fixed point - * number rounding down. Revert on overflow. - * - * @param x signed 128.128-bin fixed point number - * @return signed 64.64-bit fixed point number - */ - function from128x128(int256 x) internal pure returns (int128) { - int256 result = x >> 64; - require(result >= MIN_64x64 && result <= MAX_64x64); - return int128(result); - } - - /** - * Convert signed 64.64 fixed point number into signed 128.128 fixed point - * number. - * - * @param x signed 64.64-bit fixed point number - * @return signed 128.128 fixed point number - */ - function to128x128(int128 x) internal pure returns (int256) { - return int256(x) << 64; - } - - /** - * Calculate x + y. Revert on overflow. - * - * @param x signed 64.64-bit fixed point number - * @param y signed 64.64-bit fixed point number - * @return signed 64.64-bit fixed point number - */ - function add(int128 x, int128 y) internal pure returns (int128) { - int256 result = int256(x) + y; - require(result >= MIN_64x64 && result <= MAX_64x64); - return int128(result); - } - - /** - * Calculate x - y. Revert on overflow. - * - * @param x signed 64.64-bit fixed point number - * @param y signed 64.64-bit fixed point number - * @return signed 64.64-bit fixed point number - */ - function sub(int128 x, int128 y) internal pure returns (int128) { - int256 result = int256(x) - y; - require(result >= MIN_64x64 && result <= MAX_64x64); - return int128(result); - } - - /** - * Calculate x * y rounding down. Revert on overflow. - * - * @param x signed 64.64-bit fixed point number - * @param y signed 64.64-bit fixed point number - * @return signed 64.64-bit fixed point number - */ - function mul(int128 x, int128 y) internal pure returns (int128) { - int256 result = (int256(x) * y) >> 64; - require(result >= MIN_64x64 && result <= MAX_64x64, "ABDK - mul failed"); - return int128(result); - } - - /** - * Calculate x * y rounding towards zero, where x is signed 64.64 fixed point - * number and y is signed 256-bit integer number. Revert on overflow. - * - * @param x signed 64.64 fixed point number - * @param y signed 256-bit integer number - * @return signed 256-bit integer number - */ - function muli(int128 x, int256 y) internal pure returns (int256) { - if (x == MIN_64x64) { - require( - y >= -0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF && - y <= 0x1000000000000000000000000000000000000000000000000 - ); - return -y << 63; - } else { - bool negativeResult = false; - if (x < 0) { - x = -x; - negativeResult = true; - } - if (y < 0) { - y = -y; // We rely on overflow behavior here - negativeResult = !negativeResult; - } - uint256 absoluteResult = mulu(x, uint256(y)); - if (negativeResult) { - require( - absoluteResult <= - 0x8000000000000000000000000000000000000000000000000000000000000000 - ); - return -int256(absoluteResult); // We rely on overflow behavior here - } else { - require( - absoluteResult <= - 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - ); - return int256(absoluteResult); - } - } - } - - /** - * Calculate x * y rounding down, where x is signed 64.64 fixed point number - * and y is unsigned 256-bit integer number. Revert on overflow. - * - * @param x signed 64.64 fixed point number - * @param y unsigned 256-bit integer number - * @return unsigned 256-bit integer number - */ - function mulu(int128 x, uint256 y) internal pure returns (uint256) { - if (y == 0) return 0; - - require(x >= 0, "mulu"); - - uint256 lo = (uint256(x) * (y & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)) >> - 64; - uint256 hi = uint256(x) * (y >> 128); - - require(hi <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, "mulu"); - hi <<= 64; - - require( - hi <= - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - - lo - , - "mulu"); - return hi + lo; - } - - /** - * Calculate x / y rounding towards zero. Revert on overflow or when y is - * zero. - * - * @param x signed 64.64-bit fixed point number - * @param y signed 64.64-bit fixed point number - * @return signed 64.64-bit fixed point number - */ - function div(int128 x, int128 y) internal pure returns (int128) { - require(y != 0, "ABDK - div failed, y != 0"); - int256 result = (int256(x) << 64) / y; - require(result >= MIN_64x64 && result <= MAX_64x64); - return int128(result); - } - - /** - * Calculate x / y rounding towards zero, where x and y are signed 256-bit - * integer numbers. Revert on overflow or when y is zero. - * - * @param x signed 256-bit integer number - * @param y signed 256-bit integer number - * @return signed 64.64-bit fixed point number - */ - function divi(int256 x, int256 y) internal pure returns (int128) { - require(y != 0); - - bool negativeResult = false; - if (x < 0) { - x = -x; // We rely on overflow behavior here - negativeResult = true; - } - if (y < 0) { - y = -y; // We rely on overflow behavior here - negativeResult = !negativeResult; - } - uint128 absoluteResult = divuu(uint256(x), uint256(y)); - if (negativeResult) { - require(absoluteResult <= 0x80000000000000000000000000000000); - return -int128(absoluteResult); // We rely on overflow behavior here - } else { - require(absoluteResult <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF); - return int128(absoluteResult); // We rely on overflow behavior here - } - } - - /** - * Calculate x / y rounding towards zero, where x and y are unsigned 256-bit - * integer numbers. Revert on overflow or when y is zero. - * - * @param x unsigned 256-bit integer number - * @param y unsigned 256-bit integer number - * @return signed 64.64-bit fixed point number - */ - function divu(uint256 x, uint256 y) internal pure returns (int128) { - require(y != 0, "ABDK - divu failed, y != 0"); - uint128 result = divuu(x, y); - require(result <= uint128(MAX_64x64)); - return int128(result); - } - - /** - * Calculate -x. Revert on overflow. - * - * @param x signed 64.64-bit fixed point number - * @return signed 64.64-bit fixed point number - */ - function neg(int128 x) internal pure returns (int128) { - require(x != MIN_64x64); - return -x; - } - - /** - * Calculate |x|. Revert on overflow. - * - * @param x signed 64.64-bit fixed point number - * @return signed 64.64-bit fixed point number - */ - function abs(int128 x) internal pure returns (int128) { - require(x != MIN_64x64); - return x < 0 ? -x : x; - } - - /** - * Calculate 1 / x rounding towards zero. Revert on overflow or when x is - * zero. - * - * @param x signed 64.64-bit fixed point number - * @return signed 64.64-bit fixed point number - */ - function inv(int128 x) internal pure returns (int128) { - require(x != 0); - int256 result = int256(0x100000000000000000000000000000000) / x; - require(result >= MIN_64x64 && result <= MAX_64x64); - return int128(result); - } - - /** - * Calculate arithmetics average of x and y, i.e. (x + y) / 2 rounding down. - * - * @param x signed 64.64-bit fixed point number - * @param y signed 64.64-bit fixed point number - * @return signed 64.64-bit fixed point number - */ - function avg(int128 x, int128 y) internal pure returns (int128) { - return int128((int256(x) + int256(y)) >> 1); - } - - /** - * Calculate geometric average of x and y, i.e. sqrt (x * y) rounding down. - * Revert on overflow or in case x * y is negative. - * - * @param x signed 64.64-bit fixed point number - * @param y signed 64.64-bit fixed point number - * @return signed 64.64-bit fixed point number - */ - function gavg(int128 x, int128 y) internal pure returns (int128) { - int256 m = int256(x) * int256(y); - require(m >= 0); - require( - m < - 0x4000000000000000000000000000000000000000000000000000000000000000 - ); - return int128(sqrtu(uint256(m))); - } - - /** - * Calculate x^y assuming 0^0 is 1, where x is signed 64.64 fixed point number - * and y is unsigned 256-bit integer number. Revert on overflow. - * - * @param x signed 64.64-bit fixed point number - * @param y uint256 value - * @return signed 64.64-bit fixed point number - */ - function pow(int128 x, uint256 y) internal pure returns (int128) { - uint256 absoluteResult; - bool negativeResult = false; - if (x >= 0) { - absoluteResult = powu(uint256(x) << 63, y); - } else { - // We rely on overflow behavior here - absoluteResult = powu(uint256(uint128(-x)) << 63, y); - negativeResult = y & 1 > 0; - } - - absoluteResult >>= 63; - - if (negativeResult) { - require(absoluteResult <= 0x80000000000000000000000000000000); - return -int128(absoluteResult); // We rely on overflow behavior here - } else { - require(absoluteResult <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF); - return int128(absoluteResult); // We rely on overflow behavior here - } - } - - /** - * Calculate sqrt (x) rounding down. Revert if x < 0. - * - * @param x signed 64.64-bit fixed point number - * @return signed 64.64-bit fixed point number - */ - function sqrt(int128 x) internal pure returns (int128) { - require(x >= 0); - return int128(sqrtu(uint256(x) << 64)); - } - - /** - * Calculate binary logarithm of x. Revert if x <= 0. - * - * @param x signed 64.64-bit fixed point number - * @return signed 64.64-bit fixed point number - */ - function log_2(int128 x) internal pure returns (int128) { - require(x > 0); - - int256 msb = 0; - int256 xc = x; - if (xc >= 0x10000000000000000) { - xc >>= 64; - msb += 64; - } - if (xc >= 0x100000000) { - xc >>= 32; - msb += 32; - } - if (xc >= 0x10000) { - xc >>= 16; - msb += 16; - } - if (xc >= 0x100) { - xc >>= 8; - msb += 8; - } - if (xc >= 0x10) { - xc >>= 4; - msb += 4; - } - if (xc >= 0x4) { - xc >>= 2; - msb += 2; - } - if (xc >= 0x2) msb += 1; // No need to shift xc anymore - - int256 result = (msb - 64) << 64; - uint256 ux = uint256(x) << uint256(127 - msb); - for (int256 bit = 0x8000000000000000; bit > 0; bit >>= 1) { - ux *= ux; - uint256 b = ux >> 255; - ux >>= 127 + b; - result += bit * int256(b); - } - - return int128(result); - } - - /** - * Calculate natural logarithm of x. Revert if x <= 0. - * - * @param x signed 64.64-bit fixed point number - * @return signed 64.64-bit fixed point number - */ - function ln(int128 x) internal pure returns (int128) { - require(x > 0); - - return - int128( - (uint256(log_2(x)) * 0xB17217F7D1CF79ABC9E3B39803F2F6AF) >> 128 - ); - } - - /** - * Calculate binary exponent of x. Revert on overflow. - * - * @param x signed 64.64-bit fixed point number - * @return signed 64.64-bit fixed point number - */ - function exp_2(int128 x) internal pure returns (int128) { - require(x < 0x400000000000000000); // Overflow - - if (x < -0x400000000000000000) return 0; // Underflow - - uint256 result = 0x80000000000000000000000000000000; - - if (x & 0x8000000000000000 > 0) - result = (result * 0x16A09E667F3BCC908B2FB1366EA957D3E) >> 128; - if (x & 0x4000000000000000 > 0) - result = (result * 0x1306FE0A31B7152DE8D5A46305C85EDEC) >> 128; - if (x & 0x2000000000000000 > 0) - result = (result * 0x1172B83C7D517ADCDF7C8C50EB14A791F) >> 128; - if (x & 0x1000000000000000 > 0) - result = (result * 0x10B5586CF9890F6298B92B71842A98363) >> 128; - if (x & 0x800000000000000 > 0) - result = (result * 0x1059B0D31585743AE7C548EB68CA417FD) >> 128; - if (x & 0x400000000000000 > 0) - result = (result * 0x102C9A3E778060EE6F7CACA4F7A29BDE8) >> 128; - if (x & 0x200000000000000 > 0) - result = (result * 0x10163DA9FB33356D84A66AE336DCDFA3F) >> 128; - if (x & 0x100000000000000 > 0) - result = (result * 0x100B1AFA5ABCBED6129AB13EC11DC9543) >> 128; - if (x & 0x80000000000000 > 0) - result = (result * 0x10058C86DA1C09EA1FF19D294CF2F679B) >> 128; - if (x & 0x40000000000000 > 0) - result = (result * 0x1002C605E2E8CEC506D21BFC89A23A00F) >> 128; - if (x & 0x20000000000000 > 0) - result = (result * 0x100162F3904051FA128BCA9C55C31E5DF) >> 128; - if (x & 0x10000000000000 > 0) - result = (result * 0x1000B175EFFDC76BA38E31671CA939725) >> 128; - if (x & 0x8000000000000 > 0) - result = (result * 0x100058BA01FB9F96D6CACD4B180917C3D) >> 128; - if (x & 0x4000000000000 > 0) - result = (result * 0x10002C5CC37DA9491D0985C348C68E7B3) >> 128; - if (x & 0x2000000000000 > 0) - result = (result * 0x1000162E525EE054754457D5995292026) >> 128; - if (x & 0x1000000000000 > 0) - result = (result * 0x10000B17255775C040618BF4A4ADE83FC) >> 128; - if (x & 0x800000000000 > 0) - result = (result * 0x1000058B91B5BC9AE2EED81E9B7D4CFAB) >> 128; - if (x & 0x400000000000 > 0) - result = (result * 0x100002C5C89D5EC6CA4D7C8ACC017B7C9) >> 128; - if (x & 0x200000000000 > 0) - result = (result * 0x10000162E43F4F831060E02D839A9D16D) >> 128; - if (x & 0x100000000000 > 0) - result = (result * 0x100000B1721BCFC99D9F890EA06911763) >> 128; - if (x & 0x80000000000 > 0) - result = (result * 0x10000058B90CF1E6D97F9CA14DBCC1628) >> 128; - if (x & 0x40000000000 > 0) - result = (result * 0x1000002C5C863B73F016468F6BAC5CA2B) >> 128; - if (x & 0x20000000000 > 0) - result = (result * 0x100000162E430E5A18F6119E3C02282A5) >> 128; - if (x & 0x10000000000 > 0) - result = (result * 0x1000000B1721835514B86E6D96EFD1BFE) >> 128; - if (x & 0x8000000000 > 0) - result = (result * 0x100000058B90C0B48C6BE5DF846C5B2EF) >> 128; - if (x & 0x4000000000 > 0) - result = (result * 0x10000002C5C8601CC6B9E94213C72737A) >> 128; - if (x & 0x2000000000 > 0) - result = (result * 0x1000000162E42FFF037DF38AA2B219F06) >> 128; - if (x & 0x1000000000 > 0) - result = (result * 0x10000000B17217FBA9C739AA5819F44F9) >> 128; - if (x & 0x800000000 > 0) - result = (result * 0x1000000058B90BFCDEE5ACD3C1CEDC823) >> 128; - if (x & 0x400000000 > 0) - result = (result * 0x100000002C5C85FE31F35A6A30DA1BE50) >> 128; - if (x & 0x200000000 > 0) - result = (result * 0x10000000162E42FF0999CE3541B9FFFCF) >> 128; - if (x & 0x100000000 > 0) - result = (result * 0x100000000B17217F80F4EF5AADDA45554) >> 128; - if (x & 0x80000000 > 0) - result = (result * 0x10000000058B90BFBF8479BD5A81B51AD) >> 128; - if (x & 0x40000000 > 0) - result = (result * 0x1000000002C5C85FDF84BD62AE30A74CC) >> 128; - if (x & 0x20000000 > 0) - result = (result * 0x100000000162E42FEFB2FED257559BDAA) >> 128; - if (x & 0x10000000 > 0) - result = (result * 0x1000000000B17217F7D5A7716BBA4A9AE) >> 128; - if (x & 0x8000000 > 0) - result = (result * 0x100000000058B90BFBE9DDBAC5E109CCE) >> 128; - if (x & 0x4000000 > 0) - result = (result * 0x10000000002C5C85FDF4B15DE6F17EB0D) >> 128; - if (x & 0x2000000 > 0) - result = (result * 0x1000000000162E42FEFA494F1478FDE05) >> 128; - if (x & 0x1000000 > 0) - result = (result * 0x10000000000B17217F7D20CF927C8E94C) >> 128; - if (x & 0x800000 > 0) - result = (result * 0x1000000000058B90BFBE8F71CB4E4B33D) >> 128; - if (x & 0x400000 > 0) - result = (result * 0x100000000002C5C85FDF477B662B26945) >> 128; - if (x & 0x200000 > 0) - result = (result * 0x10000000000162E42FEFA3AE53369388C) >> 128; - if (x & 0x100000 > 0) - result = (result * 0x100000000000B17217F7D1D351A389D40) >> 128; - if (x & 0x80000 > 0) - result = (result * 0x10000000000058B90BFBE8E8B2D3D4EDE) >> 128; - if (x & 0x40000 > 0) - result = (result * 0x1000000000002C5C85FDF4741BEA6E77E) >> 128; - if (x & 0x20000 > 0) - result = (result * 0x100000000000162E42FEFA39FE95583C2) >> 128; - if (x & 0x10000 > 0) - result = (result * 0x1000000000000B17217F7D1CFB72B45E1) >> 128; - if (x & 0x8000 > 0) - result = (result * 0x100000000000058B90BFBE8E7CC35C3F0) >> 128; - if (x & 0x4000 > 0) - result = (result * 0x10000000000002C5C85FDF473E242EA38) >> 128; - if (x & 0x2000 > 0) - result = (result * 0x1000000000000162E42FEFA39F02B772C) >> 128; - if (x & 0x1000 > 0) - result = (result * 0x10000000000000B17217F7D1CF7D83C1A) >> 128; - if (x & 0x800 > 0) - result = (result * 0x1000000000000058B90BFBE8E7BDCBE2E) >> 128; - if (x & 0x400 > 0) - result = (result * 0x100000000000002C5C85FDF473DEA871F) >> 128; - if (x & 0x200 > 0) - result = (result * 0x10000000000000162E42FEFA39EF44D91) >> 128; - if (x & 0x100 > 0) - result = (result * 0x100000000000000B17217F7D1CF79E949) >> 128; - if (x & 0x80 > 0) - result = (result * 0x10000000000000058B90BFBE8E7BCE544) >> 128; - if (x & 0x40 > 0) - result = (result * 0x1000000000000002C5C85FDF473DE6ECA) >> 128; - if (x & 0x20 > 0) - result = (result * 0x100000000000000162E42FEFA39EF366F) >> 128; - if (x & 0x10 > 0) - result = (result * 0x1000000000000000B17217F7D1CF79AFA) >> 128; - if (x & 0x8 > 0) - result = (result * 0x100000000000000058B90BFBE8E7BCD6D) >> 128; - if (x & 0x4 > 0) - result = (result * 0x10000000000000002C5C85FDF473DE6B2) >> 128; - if (x & 0x2 > 0) - result = (result * 0x1000000000000000162E42FEFA39EF358) >> 128; - if (x & 0x1 > 0) - result = (result * 0x10000000000000000B17217F7D1CF79AB) >> 128; - - result >>= uint256(63 - (x >> 64)); - require(result <= uint256(MAX_64x64)); - - return int128(result); - } - - /** - * Calculate natural exponent of x. Revert on overflow. - * - * @param x signed 64.64-bit fixed point number - * @return signed 64.64-bit fixed point number - */ - function exp(int128 x) internal pure returns (int128) { - require(x < 0x400000000000000000, "ABDK - exp failed"); // Overflow - - if (x < -0x400000000000000000) return 0; // Underflow - - return - exp_2( - int128((int256(x) * 0x171547652B82FE1777D0FFDA0D23A7D12) >> 128) - ); - } - - /** - * Calculate x / y rounding towards zero, where x and y are unsigned 256-bit - * integer numbers. Revert on overflow or when y is zero. - * - * @param x unsigned 256-bit integer number - * @param y unsigned 256-bit integer number - * @return unsigned 64.64-bit fixed point number - */ - function divuu(uint256 x, uint256 y) private pure returns (uint128) { - require(y != 0); - - uint256 result; - - if (x <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - result = (x << 64) / y; - else { - uint256 msb = 192; - uint256 xc = x >> 192; - if (xc >= 0x100000000) { - xc >>= 32; - msb += 32; - } - if (xc >= 0x10000) { - xc >>= 16; - msb += 16; - } - if (xc >= 0x100) { - xc >>= 8; - msb += 8; - } - if (xc >= 0x10) { - xc >>= 4; - msb += 4; - } - if (xc >= 0x4) { - xc >>= 2; - msb += 2; - } - if (xc >= 0x2) msb += 1; // No need to shift xc anymore - - result = (x << (255 - msb)) / (((y - 1) >> (msb - 191)) + 1); - require(result <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF); - - uint256 hi = result * (y >> 128); - uint256 lo = result * (y & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF); - - uint256 xh = x >> 192; - uint256 xl = x << 64; - - if (xl < lo) xh -= 1; - xl -= lo; // We rely on overflow behavior here - lo = hi << 128; - if (xl < lo) xh -= 1; - xl -= lo; // We rely on overflow behavior here - - assert(xh == hi >> 128); - - result += xl / y; - } - - require(result <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF); - return uint128(result); - } - - /** - * Calculate x^y assuming 0^0 is 1, where x is unsigned 129.127 fixed point - * number and y is unsigned 256-bit integer number. Revert on overflow. - * - * @param x unsigned 129.127-bit fixed point number - * @param y uint256 value - * @return unsigned 129.127-bit fixed point number - */ - function powu(uint256 x, uint256 y) private pure returns (uint256) { - if (y == 0) return 0x80000000000000000000000000000000; - else if (x == 0) return 0; - else { - int256 msb = 0; - uint256 xc = x; - if (xc >= 0x100000000000000000000000000000000) { - xc >>= 128; - msb += 128; - } - if (xc >= 0x10000000000000000) { - xc >>= 64; - msb += 64; - } - if (xc >= 0x100000000) { - xc >>= 32; - msb += 32; - } - if (xc >= 0x10000) { - xc >>= 16; - msb += 16; - } - if (xc >= 0x100) { - xc >>= 8; - msb += 8; - } - if (xc >= 0x10) { - xc >>= 4; - msb += 4; - } - if (xc >= 0x4) { - xc >>= 2; - msb += 2; - } - if (xc >= 0x2) msb += 1; // No need to shift xc anymore - - int256 xe = msb - 127; - if (xe > 0) x >>= uint256(xe); - else x <<= uint256(-xe); - - uint256 result = 0x80000000000000000000000000000000; - int256 re = 0; - - while (y > 0) { - if (y & 1 > 0) { - result = result * x; - y -= 1; - re += xe; - if ( - result >= - 0x8000000000000000000000000000000000000000000000000000000000000000 - ) { - result >>= 128; - re += 1; - } else result >>= 127; - if (re < -127) return 0; // Underflow - require(re < 128); // Overflow - } else { - x = x * x; - y >>= 1; - xe <<= 1; - if ( - x >= - 0x8000000000000000000000000000000000000000000000000000000000000000 - ) { - x >>= 128; - xe += 1; - } else x >>= 127; - if (xe < -127) return 0; // Underflow - require(xe < 128); // Overflow - } - } - - if (re > 0) result <<= uint256(re); - else if (re < 0) result >>= uint256(-re); - - return result; - } - } - - /** - * Calculate sqrt (x) rounding down, where x is unsigned 256-bit integer - * number. - * - * @param x unsigned 256-bit integer number - * @return unsigned 128-bit integer number - */ - function sqrtu(uint256 x) private pure returns (uint128) { - if (x == 0) return 0; - else { - uint256 xx = x; - uint256 r = 1; - if (xx >= 0x100000000000000000000000000000000) { - xx >>= 128; - r <<= 64; - } - if (xx >= 0x10000000000000000) { - xx >>= 64; - r <<= 32; - } - if (xx >= 0x100000000) { - xx >>= 32; - r <<= 16; - } - if (xx >= 0x10000) { - xx >>= 16; - r <<= 8; - } - if (xx >= 0x100) { - xx >>= 8; - r <<= 4; - } - if (xx >= 0x10) { - xx >>= 4; - r <<= 2; - } - if (xx >= 0x8) { - r <<= 1; - } - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; - r = (r + x / r) >> 1; // Seven iterations should be enough - uint256 r1 = x / r; - return uint128(r < r1 ? r : r1); - } - } -} diff --git a/contracts/libraries/TransferHelper.sol b/contracts/libraries/TransferHelper.sol deleted file mode 100644 index b381b6f..0000000 --- a/contracts/libraries/TransferHelper.sol +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.6.0; - -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -library TransferHelper { - /// @notice Transfers tokens from the targeted address to the given destination - /// @notice Errors with 'STF' if transfer fails - /// @param token The contract address of the token to be transferred - /// @param from The originating address from which the tokens will be transferred - /// @param to The destination address of the transfer - /// @param value The amount to be transferred - function safeTransferFrom( - address token, - address from, - address to, - uint256 value - ) internal { - (bool success, bytes memory data) = token.call( - abi.encodeWithSelector( - IERC20.transferFrom.selector, - from, - to, - value - ) - ); - require( - success && (data.length == 0 || abi.decode(data, (bool))), - "STF" - ); - } - - /// @notice Transfers tokens from msg.sender to a recipient - /// @dev Errors with ST if transfer fails - /// @param token The contract address of the token which will be transferred - /// @param to The recipient of the transfer - /// @param value The value of the transfer - function safeTransfer( - address token, - address to, - uint256 value - ) internal { - (bool success, bytes memory data) = token.call( - abi.encodeWithSelector(IERC20.transfer.selector, to, value) - ); - require( - success && (data.length == 0 || abi.decode(data, (bool))), - "ST" - ); - } - - /// @notice Approves the stipulated contract to spend the given allowance in the given token - /// @dev Errors with 'SA' if transfer fails - /// @param token The contract address of the token to be approved - /// @param to The target of the approval - /// @param value The amount of the given token the target will be allowed to spend - function safeApprove( - address token, - address to, - uint256 value - ) internal { - (bool success, bytes memory data) = token.call( - abi.encodeWithSelector(IERC20.approve.selector, to, value) - ); - require( - success && (data.length == 0 || abi.decode(data, (bool))), - "SA" - ); - } - - /// @notice Transfers ETH to the recipient address - /// @dev Fails with `STE` - /// @param to The destination of the transfer - /// @param value The value to be transferred - function safeTransferETH(address to, uint256 value) internal { - (bool success, ) = to.call{value: value}(new bytes(0)); - require(success, "STE"); - } -} diff --git a/contracts/peripheral_contracts/VestingWallet.sol b/contracts/peripheral_contracts/VestingWallet.sol new file mode 100644 index 0000000..a8b0386 --- /dev/null +++ b/contracts/peripheral_contracts/VestingWallet.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.6.0) (finance/VestingWallet.sol) +pragma solidity ^0.7.6; + +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/utils/Context.sol"; +import "@openzeppelin/contracts/math/Math.sol"; + +/** + * @title VestingWallet + * @dev This contract handles the vesting of Eth and ERC20 tokens for a given beneficiary. Custody of multiple tokens + * can be given to this contract, which will release the token to the beneficiary following a given vesting schedule. + * The vesting schedule is customizable through the {vestedAmount} function. + * + * Any token transferred to this contract will follow the vesting schedule as if they were locked from the beginning. + * Consequently, if the vesting has already started, any amount of tokens sent to this contract will (at least partly) + * be immediately releasable. + */ +contract VestingWallet is Context { + event EtherReleased(uint256 amount); + event ERC20Released(address indexed token, uint256 amount); + + uint256 private _released; + mapping(address => uint256) private _erc20Released; + address private immutable _beneficiary; + uint64 private immutable _start; + uint64 private immutable _duration; + + /** + * @dev Set the beneficiary, start timestamp and vesting duration of the vesting wallet. + */ + constructor( + address beneficiaryAddress, + uint64 startTimestamp, + uint64 durationSeconds + ) { + require( + beneficiaryAddress != address(0), + "VestingWallet: beneficiary is zero address" + ); + _beneficiary = beneficiaryAddress; + _start = startTimestamp; + _duration = durationSeconds; + } + + /** + * @dev The contract should be able to receive Eth. + */ + receive() external payable virtual {} + + /** + * @dev Getter for the beneficiary address. + */ + function beneficiary() public view virtual returns (address) { + return _beneficiary; + } + + /** + * @dev Getter for the start timestamp. + */ + function start() public view virtual returns (uint256) { + return _start; + } + + /** + * @dev Getter for the vesting duration. + */ + function duration() public view virtual returns (uint256) { + return _duration; + } + + /** + * @dev Amount of eth already released + */ + function released() public view virtual returns (uint256) { + return _released; + } + + /** + * @dev Amount of token already released + */ + function released(address token) public view virtual returns (uint256) { + return _erc20Released[token]; + } + + /** + * @dev Release the native token (ether) that have already vested. + * + * Emits a {TokensReleased} event. + */ + function release() public virtual { + uint256 releasable = vestedAmount(uint64(block.timestamp)) - released(); + _released += releasable; + emit EtherReleased(releasable); + Address.sendValue(payable(beneficiary()), releasable); + } + + /** + * @dev Release the tokens that have already vested. + * + * Emits a {TokensReleased} event. + */ + function release(address token) public virtual { + uint256 releasable = vestedAmount(token, uint64(block.timestamp)) - + released(token); + _erc20Released[token] += releasable; + emit ERC20Released(token, releasable); + SafeERC20.safeTransfer(IERC20(token), beneficiary(), releasable); + } + + /** + * @dev Calculates the amount of ether that has already vested. Default implementation is a linear vesting curve. + */ + function vestedAmount(uint64 timestamp) + public + view + virtual + returns (uint256) + { + return _vestingSchedule(address(this).balance + released(), timestamp); + } + + /** + * @dev Calculates the amount of tokens that has already vested. Default implementation is a linear vesting curve. + */ + function vestedAmount(address token, uint64 timestamp) + public + view + virtual + returns (uint256) + { + return + _vestingSchedule( + IERC20(token).balanceOf(address(this)) + released(token), + timestamp + ); + } + + /** + * @dev Virtual implementation of the vesting formula. This returns the amount vested, as a function of time, for + * an asset given its total historical allocation. + */ + function _vestingSchedule(uint256 totalAllocation, uint64 timestamp) + internal + view + virtual + returns (uint256) + { + if (timestamp < start()) { + return 0; + } else if (timestamp > start() + duration()) { + return totalAllocation; + } else { + return (totalAllocation * (timestamp - start())) / duration(); + } + } +} diff --git a/contracts/rubiconPools/BathHouse.sol b/contracts/rubiconPools/BathHouse.sol index 297e2f1..fc603d4 100644 --- a/contracts/rubiconPools/BathHouse.sol +++ b/contracts/rubiconPools/BathHouse.sol @@ -1,13 +1,11 @@ // SPDX-License-Identifier: BUSL-1.1 -/// @author Benjamin Hughes - Rubicon -/// @notice This contract acts as the admin for the Rubicon Pools system -/// @notice The BathHouse approves library contracts and initializes bathTokens -/// @notice this contract has protocol-wide, sensitive, and useful getters to map assets <> bathTokens TODO +/// @title The administrator contract of Rubicon Pools +/// @author Rubicon DeFi Inc. - bghughes.eth +/// @notice The BathHouse initializes proxy-wrapped bathTokens, manages approved strategists, and sets system variables pragma solidity =0.7.6; -// import "./BathPair.sol"; import "./BathToken.sol"; import "../interfaces/IBathPair.sol"; import "../interfaces/IBathToken.sol"; @@ -16,39 +14,54 @@ import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/proxy/TransparentUpgradeableProxy.sol"; contract BathHouse { + /// *** Storage Variables *** + + /// @notice Rubicon Bath House string public name; + /// @notice The administrator of the Bath House contract address public admin; + + /// @notice The proxy administrator of Bath Tokens address public proxyManager; + /// @notice The core Rubicon Market of the Pools system address public RubiconMarketAddress; - // List of approved strategies + /// @notice A mapping of approved strategists to access Pools liquidity mapping(address => bool) public approvedStrategists; + /// @notice The initialization status of BathHouse bool public initialized; - bool public permissionedStrategists; //if true strategists are permissioned - // Key, system-wide risk parameters for Pools - uint256 public reserveRatio; // proportion of the pool that must remain present in the pair + /// @notice If true, strategists are permissioned and must be approved by admin + bool public permissionedStrategists; + + /// @notice Key, system-wide risk parameter for all liquity Pools + /// @notice This represents the proportion of a pool's underlying assets that must remain in the pool + /// @dev This protects a run on the bank scenario and ensures users can withdraw while allowing funds to be utilized for yield in the market + uint256 public reserveRatio; - // The delay after which unfilled orders are cancelled - uint256 public timeDelay; // *Deprecate post Optimism*??? Presently unused + /// @notice A variable time delay after which a strategist must return funds to the Bath Token + uint256 public timeDelay; - //NEW - address public approvedPairContract; // Ensure that only one BathPair.sol is in operation + /// @notice The lone Bath Pair contract of the system which acts as the strategist entry point and logic contract + address public approvedPairContract; - //NEW + /// @notice The basis point fee that is paid to strategists from LPs on capital that is successfully rebalanced to a Bath Token uint8 public bpsToStrategists; - //NEW - mapping(address => address) public tokenToBathToken; //Source of truth mapping that logs all ERC20 Liquidity pools underlying asset => bathToken Address + /// @notice Key mapping for determining the address of a Bath Token based on its underlying asset + /// @dev Source of truth mapping that logs all ERC20 Liquidity pools underlying asset => bathToken Address + mapping(address => address) public tokenToBathToken; - // The BathToken.sol implementation that any new bathTokens become + /// @notice The BathToken.sol implementation that any new bathTokens inherit + /// @dev The implementation of any ~newly spawned~ proxy-wrapped Bath Tokens via _createBathToken address public newBathTokenImplementation; - //NEW - // Event to log new BathPairs and their bathTokens + /// *** Events *** + + /// @notice An event that signals the creation of a new Bath Token event LogNewBathToken( address underlyingToken, address bathTokenAddress, @@ -57,6 +70,7 @@ contract BathHouse { address bathTokenCreator ); + /// @notice An event that signals the permissionless spawning of a new Bath Token event LogOpenCreationSignal( ERC20 newERC20Underlying, address spawnedBathToken, @@ -67,12 +81,19 @@ contract BathHouse { address signaler ); + /// *** Modifiers *** + + /// @notice This modifier enforces that only the admin can call these functions modifier onlyAdmin() { require(msg.sender == admin); _; } - /// @dev Proxy-safe initialization of storage + /// *** External Functions *** + + /// @notice The constructor-like initialization function + /// @dev Proxy-safe initialization of storage that sets key storage variables + /// @dev Admin is set to msg.sender function initialize( address market, uint256 _reserveRatio, @@ -84,81 +105,38 @@ contract BathHouse { name = "Rubicon Bath House"; admin = msg.sender; timeDelay = _timeDelay; + + // Set Bath Token reserve ratio globally require(_reserveRatio <= 100); require(_reserveRatio > 0); reserveRatio = _reserveRatio; + // Set BPS reward fee for successful strategist market-making + /// @notice [(10000 - {bpsToStrategists}) / 10000] BPS of MM-ing activity is passed to users bpsToStrategists = 20; + // Set key storage variables RubiconMarketAddress = market; - approveStrategist(admin); permissionedStrategists = true; - initialized = true; newBathTokenImplementation = _newBathTokenImplementation; proxyManager = _proxyAdmin; - } - - // ** Bath Token Actions ** - // **Logs the publicmapping of new ERC20 to resulting bathToken - function createBathToken(ERC20 underlyingERC20, address _feeAdmin) - external - onlyAdmin - returns (address newBathTokenAddress) - { - newBathTokenAddress = _createBathToken(underlyingERC20, _feeAdmin); - } - - // **Logs the publicmapping of new ERC20 to resulting bathToken - function _createBathToken(ERC20 underlyingERC20, address _feeAdmin) - internal - returns (address newBathTokenAddress) - { - require(initialized, "BathHouse not initialized"); - address _underlyingERC20 = address(underlyingERC20); - require( - _underlyingERC20 != address(0), - "Cant create bathToken for zero address" - ); - // Check that it isn't already logged in the registry - require( - tokenToBathToken[_underlyingERC20] == address(0), - "bathToken already exists" - ); - // Creates a new bathToken that is upgradeable by the proxyManager - require( - newBathTokenImplementation != address(0), - "no implementation set for bathTokens" - ); - bytes memory _initData = abi.encodeWithSignature( - "initialize(address,address,address)", - _underlyingERC20, - (RubiconMarketAddress), - (_feeAdmin) - ); - TransparentUpgradeableProxy newBathToken = new TransparentUpgradeableProxy( - newBathTokenImplementation, - proxyManager, - _initData - ); + // Automatically approve admin as an approved strategist + approveStrategist(admin); - newBathTokenAddress = address(newBathToken); - tokenToBathToken[_underlyingERC20] = newBathTokenAddress; - emit LogNewBathToken( - _underlyingERC20, - newBathTokenAddress, - _feeAdmin, - block.timestamp, - msg.sender - ); + // Complete contract instantiation + initialized = true; } - /// Permissionless entry point to spawn a bathToken while posting liquidity to a ~pair~ - /// The user must approve the bathHouse to spend their ERC20s + /// @notice Permissionless entry point to spawn a Bath Token while posting liquidity to a ~pair of Bath Tokens~ + /// @notice Please note, creating a Bath Token in this fashion ~does not~ gaurentee markets will be made for the new pair. This function signals the desire to have a new pair supported on Rubicon for strategists to consider market-making for + /// @notice The best desiredPairedAsset to select is a popular quote currency. Many traditional systems quote in USD while the ETH quote is superior - the choice is yours sweet msg.sender + /// @dev The user must approve the bathHouse to spend their ERC20s + /// @dev The user can only spawn a Bath Token for an ERC20 that is not yet in the Pools system and they must post liquidity on the other side of the pair for an ~extant Bath Token~ function openBathTokenSpawnAndSignal( ERC20 newBathTokenUnderlying, uint256 initialLiquidityNew, // Must approve this contract to spend - ERC20 desiredPairedAsset, // MUST be paired with an existing quote for v1 + ERC20 desiredPairedAsset, // Must be paired with an existing quote for v1 uint256 initialLiquidityExistingBathToken ) external returns (address newBathToken) { // Check that it doesn't already exist @@ -172,7 +150,7 @@ contract BathHouse { ); // Spawn a bathToken for the new asset - address newOne = _createBathToken(newBathTokenUnderlying, address(0)); // NOTE: address(0) as feeAdmin means fee is paid to pool + address newOne = _createBathToken(newBathTokenUnderlying, address(0)); // NOTE: address(0) as feeAdmin means fee is paid to pool holders // Deposit initial liquidity posted of newBathTokenUnderlying require( @@ -183,9 +161,11 @@ contract BathHouse { ), "Couldn't transferFrom your initial liquidity - make sure to approve BathHouse.sol" ); + newBathTokenUnderlying.approve(newOne, initialLiquidityNew); - uint256 newBTShares = IBathToken(newOne).deposit(initialLiquidityNew); - IBathToken(newOne).transfer(msg.sender, newBTShares); + + // Deposit assets and send Bath Token shares to msg.sender + IBathToken(newOne).deposit(initialLiquidityNew, msg.sender); // desiredPairedAsset must be pulled and deposited into bathToken require( @@ -201,10 +181,12 @@ contract BathHouse { pairedPool, initialLiquidityExistingBathToken ); - uint256 pairedPoolShares = IBathToken(pairedPool).deposit( - initialLiquidityExistingBathToken + + // Deposit assets and send Bath Token shares to msg.sender + IBathToken(pairedPool).deposit( + initialLiquidityExistingBathToken, + msg.sender ); - IBathToken(pairedPool).transfer(msg.sender, pairedPoolShares); // emit an event describing the new pair, underlyings and bathTokens emit LogOpenCreationSignal( @@ -220,7 +202,18 @@ contract BathHouse { newBathToken = newOne; } - // A migrationFunction that allows writing arbitrarily to tokenToBathToken + /// ** Admin-Only Functions ** + + /// @notice An admin-only function to create a new Bath Token for any ERC20 + function createBathToken(ERC20 underlyingERC20, address _feeAdmin) + external + onlyAdmin + returns (address newBathTokenAddress) + { + newBathTokenAddress = _createBathToken(underlyingERC20, _feeAdmin); + } + + /// @notice A migration function that allows the admin to write arbitrarily to tokenToBathToken function adminWriteBathToken(ERC20 overwriteERC20, address newBathToken) external onlyAdmin @@ -235,7 +228,7 @@ contract BathHouse { ); } - /// Function to initialize the ~lone~ bathPair contract for the Rubicon protocol + /// @notice Function to initialize and store the address of the ~lone~ bathPair contract for the Rubicon protocol function initBathPair( address _bathPairAddress, uint256 _maxOrderSizeBPS, @@ -256,25 +249,34 @@ contract BathHouse { approvedPairContract = newPair; } + /// @notice Admin-only function to set a new Admin function setBathHouseAdmin(address newAdmin) external onlyAdmin { admin = newAdmin; } + /// @notice Admin-only function to approve a new permissioned strategist + function approveStrategist(address strategist) public onlyAdmin { + approvedStrategists[strategist] = true; + } + + /// @notice Admin-only function to set whether or not strategists are permissioned function setPermissionedStrategists(bool _new) external onlyAdmin { permissionedStrategists = _new; } - // Setter Functions for paramters - onlyAdmin + /// @notice Admin-only function to set timeDelay function setCancelTimeDelay(uint256 value) external onlyAdmin { timeDelay = value; } + /// @notice Admin-only function to set reserveRatio function setReserveRatio(uint256 rr) external onlyAdmin { require(rr <= 100); require(rr > 0); reserveRatio = rr; } + /// @notice Admin-only function to set a Bath Token's timeDelay function setBathTokenMarket(address bathToken, address newMarket) external onlyAdmin @@ -282,6 +284,15 @@ contract BathHouse { IBathToken(bathToken).setMarket(newMarket); } + /// @notice Admin-only function to add a bonus token to a Bath Token's reward schema + function setBonusToken(address bathToken, address newBonusToken) + external + onlyAdmin + { + IBathToken(bathToken).setBonusToken(newBonusToken); + } + + /// @notice Admin-only function to set a Bath Token's Bath House admin function setBathTokenBathHouse(address bathToken, address newAdmin) external onlyAdmin @@ -289,6 +300,7 @@ contract BathHouse { IBathToken(bathToken).setBathHouse(newAdmin); } + /// @notice Admin-only function to set a Bath Token's feeBPS function setBathTokenFeeBPS(address bathToken, uint256 newBPS) external onlyAdmin @@ -296,6 +308,8 @@ contract BathHouse { IBathToken(bathToken).setFeeBPS(newBPS); } + /// @notice Admin-only function to approve the Bath Token's underlying token on the assigned market + /// @dev required in case the market address ever changes.. #battleScars function bathTokenApproveSetMarket(address targetBathToken) external onlyAdmin @@ -303,25 +317,21 @@ contract BathHouse { IBathToken(targetBathToken).approveMarket(); } - function setFeeTo(address bathToken, address feeTo) external onlyAdmin { - IBathToken(bathToken).setFeeTo(feeTo); - } - - function setBathPairMOSBPS(address bathPair, uint16 mosbps) + /// @notice Admin-only function to set a Bath Token's fee recipient (typically the Bath Token itself) + function setBathTokenFeeTo(address bathToken, address feeTo) external onlyAdmin { - IBathPair(bathPair).setMaxOrderSizeBPS(mosbps); - } - - function setBathPairSCN(address bathPair, int128 val) external onlyAdmin { - IBathPair(bathPair).setShapeCoefNum(val); + IBathToken(bathToken).setFeeTo(feeTo); } + /// @notice Admin-only function to set a Bath Token's target Rubicon Market function setMarket(address newMarket) external onlyAdmin { RubiconMarketAddress = newMarket; } + /// *** View Functions *** + // Getter Functions for parameters function getMarket() external view returns (address) { return RubiconMarketAddress; @@ -335,7 +345,7 @@ contract BathHouse { return timeDelay; } - // Return the address of any bathToken in the system with this corresponding underlying asset + /// @notice Returns the address of any bathToken in the system based on its corresponding underlying asset function getBathTokenfromAsset(ERC20 asset) public view returns (address) { return tokenToBathToken[address(asset)]; } @@ -344,7 +354,9 @@ contract BathHouse { return bpsToStrategists; } - // ** Security Checks used throughout Pools ** + /// *** System Security Checks *** + + /// @notice A function to check whether or not an address is an approved strategist function isApprovedStrategist(address wouldBeStrategist) external view @@ -360,11 +372,69 @@ contract BathHouse { } } + /// @notice A function to check whether or not an address is the approved system instance of BathPair.sol function isApprovedPair(address pair) public view returns (bool outcome) { pair == approvedPairContract ? outcome = true : outcome = false; } - function approveStrategist(address strategist) public onlyAdmin { - approvedStrategists[strategist] = true; + /// *** Internal Functions *** + + /// @dev Low-level functionality to spawn a Bath Token using the OZ Transparent Upgradeable Proxy standard + /// @param underlyingERC20 The underlying ERC-20 asset that underlies the newBathTokenAddress + /// @param _feeAdmin Recipient of pool withdrawal fees, typically the pool itself + function _createBathToken(ERC20 underlyingERC20, address _feeAdmin) + internal + returns (address newBathTokenAddress) + { + require(initialized, "BathHouse not initialized"); + address _underlyingERC20 = address(underlyingERC20); + require( + _underlyingERC20 != address(0), + "Cant create bathToken for zero address" + ); + + // Check that it isn't already logged in the registry + require( + tokenToBathToken[_underlyingERC20] == address(0), + "bathToken already exists" + ); + + // Creates a new bathToken that is upgradeable by the proxyManager + require( + newBathTokenImplementation != address(0), + "no implementation set for bathTokens" + ); + + // Note, the option of a fee recipient for pool withdrawls exists. For all pools this is set to the pool itself in production and is visible via ~feeTo~ on any respective contract + // Note, fee admin presently ignored in the Bath Token initialization() call via defaulting to itself; though, this is still upgradeable by the Bath House admin via + bytes memory _initData = abi.encodeWithSignature( + "initialize(address,address,address)", + _underlyingERC20, + (RubiconMarketAddress), + (_feeAdmin) + ); + + + TransparentUpgradeableProxy newBathToken + = new TransparentUpgradeableProxy( + newBathTokenImplementation, + proxyManager, + _initData + ); + + // New Bath Token Address + newBathTokenAddress = address(newBathToken); + + // Write to source-of-truth router mapping for this ERC-20 => Bath Token + tokenToBathToken[_underlyingERC20] = newBathTokenAddress; + + // Log Data + emit LogNewBathToken( + _underlyingERC20, + newBathTokenAddress, + _feeAdmin, + block.timestamp, + msg.sender + ); } } diff --git a/contracts/rubiconPools/BathPair.sol b/contracts/rubiconPools/BathPair.sol index e430384..06d77d9 100644 --- a/contracts/rubiconPools/BathPair.sol +++ b/contracts/rubiconPools/BathPair.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 -/// @author Benjamin Hughes - Rubicon +/// @author Rubicon DeFi Inc. - bghughes.eth /// @notice This contract allows a strategist to use user funds in order to market make for a Rubicon pair /// @notice The BathPair is the admin for the pair's liquidity and has many security checks in place /// @notice This contract is also where strategists claim rewards for successful market making @@ -13,35 +13,51 @@ import "@openzeppelin/contracts/math/SafeMath.sol"; import "../interfaces/IBathToken.sol"; import "../interfaces/IBathHouse.sol"; import "../interfaces/IRubiconMarket.sol"; -import "../libraries/ABDKMath64x64.sol"; import "../interfaces/IStrategistUtility.sol"; contract BathPair { + /// *** Libraries *** using SafeMath for uint256; using SafeMath for uint16; + /// *** Storage Variables *** + + /// @notice The Bath House admin of this contract; used with onlyBathHouse() address public bathHouse; + + /// @notice The Rubicon Market strategists direct all activity towards. There is only one market, RubiconMarket.sol, in the Rubicon Protocol address public RubiconMarketAddress; + /// @notice The initialization status of BathPair bool public initialized; - int128 internal shapeCoefNum; - uint256 public maxOrderSizeBPS; + /// @dev Keeping deprecated variables maintains consistent network-agnostic contract abis when moving to new chains and versions + int128 internal deprecatedStorageVarKept420Proxy; + + /// @notice Intentionally unused DEPRECATED STORAGE VARIABLE to maintain contiguous state on proxy-wrapped contracts. Consider it a beautiful scar of incremental progress 📈 + /// @dev Keeping deprecated variables maintains consistent network-agnostic contract abis when moving to new chains and versions + uint256 public deprecatedStorageVarKept4Proxy; - uint256 internal last_stratTrade_id; // Variable used to increment strategist trade IDs + /// @dev The id of the last StrategistTrade made by any strategist on this contract + /// @dev This value is globally unique, and increments with every trade + uint256 internal last_stratTrade_id; + /// @notice The total amount of successful offer fills that all strategists have made for a given asset mapping(address => uint256) public totalFillsPerAsset; - // Unique id => StrategistTrade created in marketMaking call + /// @notice Unique id => StrategistTrade created in marketMaking call mapping(uint256 => StrategistTrade) public strategistTrades; - // Map a strategist to their outstanding order IDs + + /// @notice Map a strategist to their outstanding order IDs mapping(address => mapping(address => mapping(address => uint256[]))) public outOffersByStrategist; - /// @dev Tracks the fill amounts on a per-asset basis of a strategist - /// Note: strategist => erc20asset => fill amount per asset; + /// @notice Tracks the market-kaing fill amounts on a per-asset basis of a strategist + /// @dev strategist => erc20asset => fill amount per asset; mapping(address => mapping(address => uint256)) public strategist2Fills; + /// *** Structs *** + struct order { uint256 pay_amt; IERC20 pay_gem; @@ -49,7 +65,6 @@ contract BathPair { IERC20 buy_gem; } - // Pair agnostic struct StrategistTrade { uint256 askId; uint256 askPayAmt; @@ -61,6 +76,9 @@ contract BathPair { uint256 timestamp; } + /// *** Events *** + + /// @notice Log a new market-making trade placed by a strategist, resulting in a StrategitTrade event LogStrategistTrade( uint256 strategistTradeID, bytes32 askId, @@ -71,6 +89,7 @@ contract BathPair { address strategist ); + /// @notice Logs the cancellation of a StrategistTrade event LogScrubbedStratTrade( uint256 strategistIDScrubbed, uint256 assetFill, @@ -81,6 +100,7 @@ contract BathPair { address bathQuoteAddress ); + /// @notice Log when a strategist claims their market-making rewards (effectively a rebate for good performance) event LogStrategistRewardClaim( address strategist, address asset, @@ -88,6 +108,9 @@ contract BathPair { uint256 timestamp ); + /// *** External Functions *** + + /// @notice Constructor-like initialization function /// @dev Proxy-safe initialization of storage function initialize(uint256 _maxOrderSizeBPS, int128 _shapeCoefNum) external @@ -105,12 +128,17 @@ contract BathPair { RubiconMarketAddress = IBathHouse(_bathHouse).getMarket(); // Shape variables for dynamic inventory management - maxOrderSizeBPS = _maxOrderSizeBPS; - shapeCoefNum = _shapeCoefNum; + /// *** DEprecate but keep storage variable on OP + deprecatedStorageVarKept4Proxy = _maxOrderSizeBPS; + + /// @dev A deprecated storage variable! Turns out order books are elegant and complex math is simply computed off-chain, and priced in on-chain orders at the speed of Ethereum L2s! + deprecatedStorageVarKept420Proxy = _shapeCoefNum; initialized = true; } + /// *** Modifiers *** + modifier onlyBathHouse() { require(msg.sender == bathHouse); _; @@ -125,6 +153,10 @@ contract BathPair { _; } + // *** Internal Functions *** + + /// @notice This function enforces that the Bath House reserveRatio (a % of underlying pool liquidity) is enforced across all pools + /// @dev This function should ensure that reserveRatio % of the underlying liquidity always remains on the Bath Token. Utilization should be 1 - reserveRatio in practice assuming strategists use all available liquidity. function enforceReserveRatio( address underlyingAsset, address underlyingQuote @@ -144,7 +176,8 @@ contract BathPair { IBathToken(bathAssetAddress).underlyingBalance().mul( IBathHouse(bathHouse).reserveRatio() ) - ).div(100) <= IERC20(underlyingAsset).balanceOf(bathAssetAddress), + ) + .div(100) <= IERC20(underlyingAsset).balanceOf(bathAssetAddress), "Failed to meet asset pool reserve ratio" ); require( @@ -152,87 +185,33 @@ contract BathPair { IBathToken(bathQuoteAddress).underlyingBalance().mul( IBathHouse(bathHouse).reserveRatio() ) - ).div(100) <= IERC20(underlyingQuote).balanceOf(bathQuoteAddress), + ) + .div(100) <= IERC20(underlyingQuote).balanceOf(bathQuoteAddress), "Failed to meet quote pool reserve ratio" ); } - function setMaxOrderSizeBPS(uint16 val) external onlyBathHouse { - maxOrderSizeBPS = val; - } - - function setShapeCoefNum(int128 val) external onlyBathHouse { - shapeCoefNum = val; - } - - // Note: the goal is to enable a means to retrieve all outstanding orders a strategist has live in the books - // This is helpful for them to manage orders as well as place any controls on strategists - function getOutstandingStrategistTrades( - address asset, - address quote, - address strategist - ) external view returns (uint256[] memory) { - return outOffersByStrategist[asset][quote][strategist]; - } - - // *** Internal Functions *** - - // Returns price denominated in the quote asset - function getMidpointPrice(address asset, address quote) - public - view - returns ( - /* asset, quote*/ - int128 - ) - { - address _RubiconMarketAddress = RubiconMarketAddress; - uint256 bestAskID = IRubiconMarket(_RubiconMarketAddress).getBestOffer( - IERC20(asset), - IERC20(quote) - ); - uint256 bestBidID = IRubiconMarket(_RubiconMarketAddress).getBestOffer( - IERC20(quote), - IERC20(asset) - ); - // Throw a zero if unable to determine midpoint from the book - if (bestAskID == 0 || bestBidID == 0) { - return 0; - } - order memory bestAsk = getOfferInfo(bestAskID); - order memory bestBid = getOfferInfo(bestBidID); - int128 midpoint = ABDKMath64x64.divu( - ( - (bestAsk.buy_amt.div(bestAsk.pay_amt)).add( - bestBid.pay_amt.div(bestBid.buy_amt) - ) - ), - 2 - ); - return midpoint; - } - - // orderID of the fill - // only log fills for each strategist - needs to be asset specific - // isAssetFill are *quotes* that result in asset yield - // A strategist's relative share of total fills is their amount of reward claim, that is logged here + /// @notice Log whenever a strategist rebalances a fill amount and log the amount while incrementing total fills for that specific asset + /// @dev Only log fills for each strategist in an asset specific manner + /// @dev Goal is to map a strategist to a fill function logFill( uint256 amt, - // bool isAssetFill, address strategist, address asset ) internal { - // Goal is to map a strategist to a fill strategist2Fills[strategist][asset] += amt; totalFillsPerAsset[asset] += amt; } + /// @notice Internal function to provide the next unique StrategistTrade ID function _next_id() internal returns (uint256) { last_stratTrade_id++; return last_stratTrade_id; } - // Note: this function results in the removal of the order from the books and it being deleted from the contract + /// @notice This function results in the removal of the Strategist Trade (bid and/or ask on Rubicon Market) from the books and it being deleted from the contract + /// @dev The local array of strategist IDs that exists for any given strategist [query via getOutstandingStrategistTrades()] acts as an acitve RAM for outstanding strategist trades + /// @dev Cancels outstanding orders and manages the ledger of outstandingAmount() on bathTokens as Strategist Trades are cancelled/scrubbed or expired function handleStratOrderAtID(uint256 id) internal { StrategistTrade memory info = strategistTrades[id]; address _asset = info.askAsset; @@ -311,7 +290,7 @@ contract BathPair { ); } - // Get offer info from Rubicon Market + /// @notice Get information about a Rubicon Market offer and return it as an order function getOfferInfo(uint256 id) internal view returns (order memory) { ( uint256 ask_amt, @@ -323,28 +302,34 @@ contract BathPair { return offerInfo; } + /// @notice A function that returns the index of uid from array + /// @dev uid must be in array for the purposes of this contract to enforce outstanding trades per strategist are tracked correctly function getIndexFromElement(uint256 uid, uint256[] storage array) internal view returns (uint256 _index) { + bool assigned = false; for (uint256 index = 0; index < array.length; index++) { if (uid == array[index]) { _index = index; + assigned = true; return _index; } } + require(assigned, "Didnt Find that element in live list, cannot scrub"); } - // *** External functions that can be called by Strategists *** + // *** External Functions - Only Approved Strategists *** + /// @notice Key entry point for strategists to use Bath Token (LP) funds to place market-making trades on the Rubicon Order Book function placeMarketMakingTrades( address[2] memory tokenPair, // ASSET, Then Quote uint256 askNumerator, // Quote / Asset uint256 askDenominator, // Asset / Quote uint256 bidNumerator, // size in ASSET uint256 bidDenominator // size in QUOTES - ) external onlyApprovedStrategist(msg.sender) returns (uint256 id) { + ) public onlyApprovedStrategist(msg.sender) returns (uint256 id) { // Require at least one order is non-zero require( (askNumerator > 0 && askDenominator > 0) || @@ -425,6 +410,56 @@ contract BathPair { ); } + /// @notice A function to batch together many placeMarketMakingTrades() in a single transaction + function batchMarketMakingTrades( + address[2] memory tokenPair, // ASSET, Then Quote + uint256[] memory askNumerators, // Quote / Asset + uint256[] memory askDenominators, // Asset / Quote + uint256[] memory bidNumerators, // size in ASSET + uint256[] memory bidDenominators // size in QUOTES + ) external onlyApprovedStrategist(msg.sender) { + require( + askNumerators.length == askDenominators.length && + askDenominators.length == bidNumerators.length && + bidNumerators.length == bidDenominators.length, + "not all order lengths match" + ); + uint256 quantity = askNumerators.length; + + for (uint256 index = 0; index < quantity; index++) { + placeMarketMakingTrades( + tokenPair, + askNumerators[index], + askDenominators[index], + bidNumerators[index], + bidDenominators[index] + ); + } + } + + /// @notice A function to requote an outstanding order and replace it with a new Strategist Trade + /// @dev Note that this function will create a new unique id for the requote'd ID due to the low-level functionality + function requote( + uint256 id, + address[2] memory tokenPair, // ASSET, Then Quote + uint256 askNumerator, // Quote / Asset + uint256 askDenominator, // Asset / Quote + uint256 bidNumerator, // size in ASSET + uint256 bidDenominator // size in QUOTES + ) external onlyApprovedStrategist(msg.sender) { + // 1. Scrub strat trade + scrubStrategistTrade(id); + + // 2. Place another + placeMarketMakingTrades( + tokenPair, + askNumerator, + askDenominator, + bidNumerator, + bidDenominator + ); + } + /// @notice - function to rebalance fill between two pools function rebalancePair( uint256 assetRebalAmt, //amount of ASSET in the quote buffer @@ -444,7 +479,7 @@ contract BathPair { "tokenToBathToken error" ); - // TODO: improve this? + // This should be localized to the bathToken in future versions uint16 stratReward = IBathHouse(_bathHouse).getBPSToStrats(); // Simply rebalance given amounts @@ -466,8 +501,8 @@ contract BathPair { } } - //Function to attempt AMM inventory risk tail off - // This function calls the strategist utility which handles the trade and returns funds to LPs + /// @notice Function to attempt inventory risk tail off on an AMM + /// @dev This function calls the strategist utility which handles the trade and returns funds to LPs function tailOff( address targetPool, address tokenToHandle, @@ -498,8 +533,7 @@ contract BathPair { ); } - // Inputs are indices through which to scrub - // Zero indexed indices! + /// @notice Cancel an outstanding strategist offers and return funds to LPs while logging fills function scrubStrategistTrade(uint256 id) public onlyApprovedStrategist(msg.sender) @@ -511,67 +545,31 @@ contract BathPair { handleStratOrderAtID(id); } - // ** EXPERIMENTAL ** - function scrubStrategistTrades(uint256[] memory ids) external { + /// @notice Batch scrub outstanding strategist trades and return funds to LPs + function scrubStrategistTrades(uint256[] memory ids) + external + onlyApprovedStrategist(msg.sender) + { for (uint256 index = 0; index < ids.length; index++) { uint256 _id = ids[index]; scrubStrategistTrade(_id); } } - // Return the largest order size that can be placed as a strategist for given pair and their liquidity pools - // ^ I believe this is the best approach given that this function call relies on knowledge of sister liquidity pool - // High level idea: here is the asset and pair I'm market-making for, what max size can I use? - function getMaxOrderSize( - address asset, - address sisterAsset, - address targetBathToken, - address sisterBathToken - ) public view returns (uint256 maxSizeAllowed) { - require(asset != sisterAsset, "provided identical asset vales GMOS"); - int128 shapeCoef = ABDKMath64x64.div(shapeCoefNum, 1000); - // Get LP balance for target asset - uint256 underlyingBalance = IERC20(asset).balanceOf(targetBathToken); - require( - underlyingBalance > 0, - "no bathToken liquidity to calculate max orderSize permissable" - ); - - // If no midpoint in book, allow **permissioned** strategist to maxOrderSize - int128 midpoint = getMidpointPrice(asset, sisterAsset); //amount in sisterAsset also can be thought of as sisterAsset per asset price in the book - if (midpoint == 0) { - return maxOrderSizeBPS.mul(underlyingBalance).div(10000); - } - int128 ratio = ABDKMath64x64.divu( - underlyingBalance, - IERC20(sisterAsset).balanceOf(sisterBathToken) - ); // Liquidity check of: assset / sisterAsset - if (ABDKMath64x64.mul(ratio, midpoint) > (2**64)) { - // Overweight asset liquidity relative to midpoint price - return maxOrderSizeBPS.mul(underlyingBalance).div(10000); - } else { - // return dynamic order size - uint256 maxSize = maxOrderSizeBPS.mul(underlyingBalance).div(10000); - int128 shapeFactor = ABDKMath64x64.exp( - ABDKMath64x64.mul( - shapeCoef, - ABDKMath64x64.inv(ABDKMath64x64.div(ratio, midpoint)) - ) - ); - uint256 dynamicSize = ABDKMath64x64.mulu(shapeFactor, maxSize); - return dynamicSize; - } - } - - // function where strategists claim rewards proportional to their quantity of fills - // Provide the pair on which you want to claim rewards - function strategistBootyClaim(address asset, address quote) external { + /// @notice Function where strategists claim rewards proportional to their quantity of fills + /// @dev This function should allow a strategist to claim ERC20s sitting on this contract (earned via rebalancing) relative to their share or strategist activity on the pair + /// @dev Provide the pair on which you want to claim rewards + function strategistBootyClaim(address asset, address quote) + external + onlyApprovedStrategist(msg.sender) + { uint256 fillCountA = strategist2Fills[msg.sender][asset]; uint256 fillCountQ = strategist2Fills[msg.sender][quote]; if (fillCountA > 0) { uint256 booty = ( fillCountA.mul(IERC20(asset).balanceOf(address(this))) - ).div(totalFillsPerAsset[asset]); + ) + .div(totalFillsPerAsset[asset]); IERC20(asset).transfer(msg.sender, booty); emit LogStrategistRewardClaim( msg.sender, @@ -585,7 +583,8 @@ contract BathPair { if (fillCountQ > 0) { uint256 booty = ( fillCountQ.mul(IERC20(quote).balanceOf(address(this))) - ).div(totalFillsPerAsset[quote]); + ) + .div(totalFillsPerAsset[quote]); IERC20(quote).transfer(msg.sender, booty); emit LogStrategistRewardClaim( msg.sender, @@ -597,7 +596,16 @@ contract BathPair { strategist2Fills[msg.sender][quote] -= fillCountQ; } } -} -// Wish list: -// - Nicer way to scrub orders in bulk + /// *** View Functions *** + + /// @notice The goal of this function is to enable a means to retrieve all outstanding orders a strategist has live in the books + /// @dev This is helpful to manage orders as well as track all strategist orders (like their RAM of StratTrade IDs) and place any would-be constraints on strategists + function getOutstandingStrategistTrades( + address asset, + address quote, + address strategist + ) public view returns (uint256[] memory) { + return outOffersByStrategist[asset][quote][strategist]; + } +} diff --git a/contracts/rubiconPools/BathToken.sol b/contracts/rubiconPools/BathToken.sol index 51a0385..1244bf1 100644 --- a/contracts/rubiconPools/BathToken.sol +++ b/contracts/rubiconPools/BathToken.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 -/// @author Benjamin Hughes - Rubicon +/// @author Rubicon DeFi Inc. - bghughes.eth /// @notice This contract represents a single-asset liquidity pool for Rubicon Pools /// @notice Any user can deposit assets into this pool and earn yield from successful strategist market making with their liquidity /// @notice This contract looks to both BathPairs and the BathHouse as its admin @@ -9,48 +9,92 @@ pragma solidity =0.7.6; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/math/SafeMath.sol"; -// import "../RubiconMarket.sol"; import "../interfaces/IBathHouse.sol"; import "../interfaces/IRubiconMarket.sol"; +import "../interfaces/IVestingWallet.sol"; contract BathToken { using SafeMath for uint256; + + /// *** Storage Variables *** + + /// @notice The initialization status of the Bath Token bool public initialized; - // ERC-20 + /// @notice ** ERC-20 ** string public symbol; - string public name; uint8 public decimals; + /// @notice The RubiconMarket.sol instance that all pool liquidity is intially directed to as market-making offers address public RubiconMarketAddress; - address public bathHouse; // admin + + /// @notice The Bath House admin of the Bath Token + address public bathHouse; + + /// @notice The withdrawal fee recipient, typically the Bath Token itself address public feeTo; - IERC20 public underlyingToken; // called "asset" in 4626 + + /// @notice The underlying ERC-20 token which is the core asset of the Bath Token vault + IERC20 public underlyingToken; + + /// @notice The basis point fee rate that is paid on withdrawing the underlyingToken and bonusTokens uint256 public feeBPS; + /// @notice ** ERC-20 ** uint256 public totalSupply; - uint256 public outstandingAmount; // quantity of underlyingToken that is in the orderbook and still in pools - uint256[] deprecatedStorageArray; // Kept in to avoid storage collision on v0 bathTokens that are upgraded - mapping(uint256 => uint256) deprecatedMapping; // Kept in to avoid storage collision on v0 bathTokens that are upgraded + /// @notice The amount of underlying deposits that are outstanding attempting market-making on the order book for yield + /// @dev quantity of underlyingToken that is in the orderbook that the pool still has a claim on + /// @dev The underlyingToken is effectively mark-to-marketed when it enters the book and it could be returned at a loss due to poor strategist performance + /// @dev outstandingAmount is NOT inclusive of any non-underlyingToken assets sitting on the Bath Tokens that have filled to here and are awaiting rebalancing to the underlyingToken by strategists + uint256 public outstandingAmount; + + /// @dev Intentionally unused DEPRECATED STORAGE VARIABLE to maintain contiguous state on proxy-wrapped contracts. Consider it a beautiful scar of incremental progress 📈 + /// @dev Keeping deprecated variables maintains consistent network-agnostic contract abis when moving to new chains and versions + uint256[] deprecatedStorageArray; // Kept in to avoid storage collision bathTokens that are proxy upgraded + + /// @dev Intentionally unused DEPRECATED STORAGE VARIABLE to maintain contiguous state on proxy-wrapped contracts. Consider it a beautiful scar of incremental progress 📈 + mapping(uint256 => uint256) deprecatedMapping; // Kept in to avoid storage collision on bathTokens that are upgraded + // ******************************************* + /// @notice ** ERC-20 ** mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance; + /// @notice EIP-2612 bytes32 public DOMAIN_SEPARATOR; - // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + /// @notice EIP-2612 + /// @dev keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; + + /// @notice EIP-2612 mapping(address => uint256) public nonces; + /// @notice Array of Bonus ERC-20 tokens that are given as liquidity incentives to pool withdrawers + address[] public bonusTokens; + + /// @notice Address of the OZ Vesting Wallet which acts as means to vest bonusToken incentives to pool HODLers + IVestingWallet public rewardsVestingWallet; + + /// *** Events *** + + /// @notice ** ERC-20 ** event Approval( address indexed owner, address indexed spender, uint256 value ); + + /// @notice ** ERC-20 ** event Transfer(address indexed from, address indexed to, uint256 value); + + /// @notice Time of Bath Token instantiation event LogInit(uint256 timeOfInit); + + /// @notice Log details about a pool deposit event LogDeposit( uint256 depositedAmt, IERC20 asset, @@ -60,6 +104,8 @@ contract BathToken { uint256 outstandingAmount, uint256 totalSupply ); + + /// @notice Log details about a pool withdraw event LogWithdraw( uint256 amountWithdrawn, IERC20 asset, @@ -71,6 +117,8 @@ contract BathToken { uint256 outstandingAmount, uint256 totalSupply ); + + /// @notice Log details about a pool rebalance event LogRebalance( IERC20 pool_asset, address destination, @@ -81,6 +129,8 @@ contract BathToken { uint256 outstandingAmount, uint256 totalSupply ); + + /// @notice Log details about a pool order canceled in the Rubicon Market book event LogPoolCancel( uint256 orderId, IERC20 pool_asset, @@ -89,6 +139,8 @@ contract BathToken { uint256 outstandingAmount, uint256 totalSupply ); + + /// @notice Log details about a pool order placed in the Rubicon Market book event LogPoolOffer( uint256 id, IERC20 pool_asset, @@ -96,6 +148,8 @@ contract BathToken { uint256 outstandingAmount, uint256 totalSupply ); + + /// @notice Log the credit to outstanding amount for funds that have been filled market-making event LogRemoveFilledTradeAmount( IERC20 pool_asset, uint256 fillAmount, @@ -104,7 +158,36 @@ contract BathToken { uint256 totalSupply ); - /// @dev Proxy-safe initialization of storage + /// @notice * EIP 4626 * + event Deposit( + address indexed caller, + address indexed owner, + uint256 assets, + uint256 shares + ); + + /// @notice * EIP 4626 * + event Withdraw( + address indexed caller, + address indexed receiver, + address indexed owner, + uint256 assets, + uint256 shares + ); + + /// @notice Log bonus token reward event + event LogClaimBonusTokn( + address indexed caller, + address indexed receiver, + address indexed owner, + uint256 assets, + uint256 shares, + IERC20 bonusToken + ); + + /// *** Constructor *** + + /// @notice Proxy-safe initialization of storage; the constructor function initialize( ERC20 token, address market, @@ -141,11 +224,15 @@ contract BathToken { IERC20(address(token)).approve(RubiconMarketAddress, 2**256 - 1); emit LogInit(block.timestamp); - feeTo = _feeTo; //BathHouse admin is initial recipient - feeBPS = 0; //Fee set to zero + feeTo = address(this); //This contract is the fee recipient, rewarding HODLers + feeBPS = 3; //Fee set to 3 BPS initially + + // Complete constract instantiation initialized = true; } + /// *** Modifiers *** + modifier onlyPair() { require( IBathHouse(bathHouse).isApprovedPair(msg.sender) == true, @@ -162,44 +249,43 @@ contract BathToken { _; } - // Admin functions, BathHouse only + /// *** External Functions - Only Bath House / Admin *** + + /// @notice Admin-only function to set a Bath Token's market address function setMarket(address newRubiconMarket) external onlyBathHouse { RubiconMarketAddress = newRubiconMarket; } + /// @notice Admin-only function to set a Bath Token's Bath House admin function setBathHouse(address newBathHouse) external onlyBathHouse { bathHouse = newBathHouse; } - // Admin only - approve my market infinitely + /// @notice Admin-only function to approve Bath Token's RubiconMarketAddress with the maximum integer value (infinite approval) function approveMarket() external onlyBathHouse { underlyingToken.approve(RubiconMarketAddress, 2**256 - 1); } + /// @notice Admin-only function to set a Bath Token's feeBPS function setFeeBPS(uint256 _feeBPS) external onlyBathHouse { feeBPS = _feeBPS; } + /// @notice Admin-only function to set a Bath Token's fee recipient, typically the pool itself function setFeeTo(address _feeTo) external onlyBathHouse { feeTo = _feeTo; } - /// The underlying ERC-20 that this bathToken handles - function underlyingERC20() external view returns (address) { - // require(initialized); - return address(underlyingToken); + /// @notice Admin-only function to add a bonus token to bonusTokens for pool incentives + function setBonusToken(address newBonusERC20) external onlyBathHouse { + bonusTokens.push(newBonusERC20); } - /// @notice returns the amount of underlying ERC20 tokens in this pool in addition to - /// any tokens that may be outstanding in the Rubicon order book - function underlyingBalance() public view returns (uint256) { - // require(initialized, "BathToken not initialized"); + /// *** External Functions - Only Approved Bath Pair / Strategist Contract *** - uint256 _pool = IERC20(underlyingToken).balanceOf(address(this)); - return _pool.add(outstandingAmount); - } + /// ** Rubicon Market Functions ** - // ** Rubicon Market Functions: ** + /// @notice The function for a strategist to cancel an outstanding Market Offer function cancel(uint256 id, uint256 amt) external onlyPair { outstandingAmount = outstandingAmount.sub(amt); IRubiconMarket(RubiconMarketAddress).cancel(id); @@ -214,6 +300,7 @@ contract BathToken { ); } + /// @notice A function called by BathPair to maintain proper accounting of outstandingAmount function removeFilledTradeAmount(uint256 amt) external onlyPair { outstandingAmount = outstandingAmount.sub(amt); emit LogRemoveFilledTradeAmount( @@ -225,7 +312,7 @@ contract BathToken { ); } - // function that places a bid/ask in the orderbook for a given pair + /// @notice The function that places a bid and/or ask in the orderbook for a given pair from this pool function placeOffer( uint256 pay_amt, ERC20 pay_gem, @@ -249,7 +336,7 @@ contract BathToken { buy_amt, buy_gem, 0, - false // TODO: is this implicit post-only? + false ); outstandingAmount = outstandingAmount.add(pay_amt); @@ -263,15 +350,15 @@ contract BathToken { return (id); } - // This function returns filled orders to the correct liquidity pool and sends strategist rewards to the Pair - // Sends non-underlyingToken fill elsewhere in the Pools system, typically it's sister asset within a trading pair (e.g. ETH-USDC) + /// @notice This function returns filled orders to the correct liquidity pool and sends strategist rewards to the Bath Pair + /// @dev Sends non-underlyingToken fill elsewhere in the Pools system, typically it's sister asset within a trading pair (e.g. ETH-USDC) + /// @dev Strategists presently accrue rewards in the filled asset not underlyingToken function rebalance( address destination, address filledAssetToRebalance, /* sister or fill asset */ uint256 stratProportion, uint256 rebalAmt ) external onlyPair { - // require(initialized); uint256 stratReward = (stratProportion.mul(rebalAmt)).div(10000); IERC20(filledAssetToRebalance).transfer( destination, @@ -291,25 +378,215 @@ contract BathToken { ); } - // ** User Entrypoints: ** - // TODO: ensure adherence to emerging vault standard - function deposit(uint256 _amount) external returns (uint256 shares) { + /// *** EIP 4626 Implementation *** + // https://eips.ethereum.org/EIPS/eip-4626#specification + + /// @notice Withdraw your bathTokens for the underlyingToken + function withdraw(uint256 _shares) + external + returns (uint256 amountWithdrawn) + { + return _withdraw(_shares, msg.sender); + } + + /// @notice * EIP 4626 * + function asset() public view returns (address assetTokenAddress) { + assetTokenAddress = address(underlyingToken); + } + + /// @notice * EIP 4626 * + function totalAssets() public view returns (uint256 totalManagedAssets) { + return underlyingBalance(); + } + + /// @notice * EIP 4626 * + function convertToShares(uint256 assets) + public + view + returns (uint256 shares) + { + // Note: Inflationary tokens may affect this logic + (totalSupply == 0) ? shares = assets : shares = ( + assets.mul(totalSupply) + ) + .div(totalAssets()); + } + + // Note: MUST NOT be inclusive of any fees that are charged against assets in the Vault. + /// @notice * EIP 4626 * + function convertToAssets(uint256 shares) + public + view + returns (uint256 assets) + { + assets = (totalAssets().mul(shares)).div(totalSupply); + } + + // Note: Unused function param to adhere to standard + /// @notice * EIP 4626 * + function maxDeposit(address receiver) + public + pure + returns (uint256 maxAssets) + { + maxAssets = 2**256 - 1; // No limit on deposits in current implementation = Max UINT + } + + /// @notice * EIP 4626 * + function previewDeposit(uint256 assets) + public + view + returns (uint256 shares) + { + // The exact same logic is used, no deposit fee - only difference is deflationary token check (rare condition and probably redundant) + shares = convertToShares(assets); + } + + // Single asset override to reflect old functionality + function deposit(uint256 assets) public returns (uint256 shares) { + // Note: msg.sender is the same throughout the same contract context + return _deposit(assets, msg.sender); + } + + /// @notice * EIP 4626 * + function deposit(uint256 assets, address receiver) + public + returns (uint256 shares) + { + return _deposit(assets, receiver); + } + + // Note: Unused function param to adhere to standard + /// @notice * EIP 4626 * + function maxMint(address receiver) public pure returns (uint256 maxShares) { + maxShares = 2**256 - 1; // No limit on shares that could be created via deposit in current implementation - Max UINT + } + + // Given I want these shares, how much do I have to deposit + /// @notice * EIP 4626 * + function previewMint(uint256 shares) public view returns (uint256 assets) { + (totalSupply == 0) ? assets = shares : assets = ( + shares.mul(totalAssets()) + ) + .div(totalSupply); + } + + // Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens. + /// @notice * EIP 4626 * + function mint(uint256 shares, address receiver) + public + returns (uint256 assets) + { + assets = previewMint(shares); + uint256 _shares = _deposit(assets, receiver); + require(_shares == shares, "did not mint expected share count"); + } + + // A user can withdraw whatever they hold + /// @notice * EIP 4626 * + function maxWithdraw(address owner) + public + view + returns (uint256 maxAssets) + { + if (totalSupply == 0) { + maxAssets = 0; + } else { + uint256 ownerShares = balanceOf[owner]; + maxAssets = convertToAssets(ownerShares); + } + } + + /// @notice * EIP 4626 * + function previewWithdraw(uint256 assets) + public + view + returns (uint256 shares) + { + if (totalSupply == 0) { + shares = 0; + } else { + uint256 amountWithdrawn; + uint256 _fee = assets.mul(feeBPS).div(10000); + amountWithdrawn = assets.sub(_fee); + shares = convertToShares(amountWithdrawn); + } + } + + /// @notice * EIP 4626 * + function withdraw( + uint256 assets, + address receiver, + address owner + ) public returns (uint256 shares) { + require( + owner == msg.sender, + "This implementation does not support non-sender owners from withdrawing user shares" + ); + uint256 expectedShares = previewWithdraw(assets); + uint256 assetsReceived = _withdraw(expectedShares, receiver); + require( + assetsReceived >= assets, + "You cannot withdraw the amount of assets you expected" + ); + shares = expectedShares; + } + + // Constraint: msg.sender is owner of shares when withdrawing + /// @notice * EIP 4626 * + function maxRedeem(address owner) public view returns (uint256 maxShares) { + return balanceOf[owner]; + } + + // Constraint: msg.sender is owner of shares when withdrawing + /// @notice * EIP 4626 * + function previewRedeem(uint256 shares) + public + view + returns (uint256 assets) + { + uint256 r = (underlyingBalance().mul(shares)).div(totalSupply); + uint256 _fee = r.mul(feeBPS).div(10000); + assets = r.sub(_fee); + } + + /// @notice * EIP 4626 * + function redeem( + uint256 shares, + address receiver, + address owner + ) public returns (uint256 assets) { + require( + owner == msg.sender, + "This implementation does not support non-sender owners from withdrawing user shares" + ); + assets = _withdraw(shares, receiver); + } + + /// *** Internal Functions *** + + /// @notice Deposit assets for the user and mint Bath Token shares to receiver + function _deposit(uint256 assets, address receiver) + internal + returns (uint256 shares) + { uint256 _pool = underlyingBalance(); uint256 _before = underlyingToken.balanceOf(address(this)); - underlyingToken.transferFrom(msg.sender, address(this), _amount); + // **Assume caller is depositor** + underlyingToken.transferFrom(msg.sender, address(this), assets); uint256 _after = underlyingToken.balanceOf(address(this)); - _amount = _after.sub(_before); // Additional check for deflationary tokens + assets = _after.sub(_before); // Additional check for deflationary tokens - shares = 0; - (totalSupply == 0) ? shares = _amount : shares = ( - _amount.mul(totalSupply) - ).div(_pool); + (totalSupply == 0) ? shares = assets : shares = ( + assets.mul(totalSupply) + ) + .div(_pool); - _mint(msg.sender, shares); - // TODO: CHECK THAT _pool UPDATES BY CALLING underlyingBalance() WHEN SENT IN EMIT + // Send shares to designated target + _mint(receiver, shares); emit LogDeposit( - _amount, + assets, underlyingToken, shares, msg.sender, @@ -317,14 +594,20 @@ contract BathToken { outstandingAmount, totalSupply ); + emit Deposit(msg.sender, msg.sender, assets, shares); } - // No rebalance implementation for lower fees and faster swaps - function withdraw(uint256 _shares) - external + /// @notice Withdraw share for the user and send underlyingToken to receiver with any accrued yield and incentive tokens + function _withdraw(uint256 _shares, address receiver) + internal returns (uint256 amountWithdrawn) { - uint256 r = (underlyingBalance().mul(_shares)).div(totalSupply); + uint256 _initialTotalSupply = totalSupply; + + // Distribute network rewards first in order to handle bonus token == underlying token case; it only releases vested tokens in this call + distributeBonusTokenRewards(receiver, _shares, _initialTotalSupply); + + uint256 r = (underlyingBalance().mul(_shares)).div(_initialTotalSupply); _burn(msg.sender, _shares); uint256 _fee = r.mul(feeBPS).div(10000); // If FeeTo == address(0) then the fee is effectively accrued by the pool @@ -332,7 +615,7 @@ contract BathToken { underlyingToken.transfer(feeTo, _fee); } amountWithdrawn = r.sub(_fee); - underlyingToken.transfer(msg.sender, amountWithdrawn); + underlyingToken.transfer(receiver, amountWithdrawn); emit LogWithdraw( amountWithdrawn, @@ -345,9 +628,69 @@ contract BathToken { outstandingAmount, totalSupply ); + emit Withdraw( + msg.sender, + receiver, + msg.sender, + amountWithdrawn, + _shares + ); + } + + /// @notice Function to distibute non-underlyingToken Bath Token incentives to pool withdrawers + /// @dev Note that bonusTokens adhere to the same feeTo and feeBPS pattern + /// @dev Note the edge case in which the bonus token is the underlyingToken, here we simply release() to the pool and skip + function distributeBonusTokenRewards( + address receiver, + uint256 sharesWithdrawn, + uint256 initialTotalSupply + ) internal { + // Verbose check: + // require(initialTotalSupply == sharesWithdrawn + totalSupply); + if (bonusTokens.length > 0) { + for (uint256 index = 0; index < bonusTokens.length; index++) { + IERC20 token = IERC20(bonusTokens[index]); + + // Pair each bonus token with an OZ Vesting wallet. Each time a user withdraws, they can release() the + // vested amount to this pool. Note, this pool must be the beneficiary of the VestingWallet. + if (rewardsVestingWallet != IVestingWallet(0)) { + rewardsVestingWallet.release(address(token)); + } + + // avoid underlyingToken == token double spend + if (address(token) == address(underlyingToken)) { + continue; // Skip because logic below fails on underlying token and release() already called so bonus tokens are already accrued and withdrawn + } + + uint256 bonusTokenBalance = token.balanceOf(address(this)); + if (bonusTokenBalance > 0) { + uint256 amount = bonusTokenBalance.mul(sharesWithdrawn).div( + initialTotalSupply + ); + // Note: Shares already burned in _withdraw + + uint256 _fee = amount.mul(feeBPS).div(10000); + // If FeeTo == address(0) then the fee is effectively accrued by the pool + if (feeTo != address(0)) { + token.transfer(feeTo, _fee); + } + uint256 amountWithdrawn = amount.sub(_fee); + token.transfer(receiver, amountWithdrawn); + + emit LogClaimBonusTokn( + msg.sender, + receiver, + msg.sender, + amountWithdrawn, + sharesWithdrawn, + token + ); + } + } + } } - // ** Internal Functions ** + /// *** ERC - 20 Standard *** function _mint(address to, uint256 value) internal { totalSupply = totalSupply.add(value); @@ -404,6 +747,7 @@ contract BathToken { return true; } + /// @notice EIP 2612 function permit( address owner, address spender, @@ -437,4 +781,18 @@ contract BathToken { ); _approve(owner, spender, value); } + + /// *** View Functions *** + + /// @notice The underlying ERC-20 that this bathToken handles + function underlyingERC20() external view returns (address) { + return address(underlyingToken); + } + + /// @notice The best-guess total claim on assets the Bath Token has + /// @dev returns the amount of underlying ERC20 tokens in this pool in addition to any tokens that are outstanding in the Rubicon order book seeking market-making yield (outstandingAmount) + function underlyingBalance() public view returns (uint256) { + uint256 _pool = IERC20(underlyingToken).balanceOf(address(this)); + return _pool.add(outstandingAmount); + } } diff --git a/test/BathHouse_test.js b/test/BathHouse_test.js index 6017509..9a1e065 100644 --- a/test/BathHouse_test.js +++ b/test/BathHouse_test.js @@ -84,11 +84,6 @@ contract("Bath House", (accounts) => { it("public uint timeDelay - set correctly", async () => { assert.equal(await bathHouseInstance.timeDelay(), 10); }); - // DEPRECATED - // it("public uint maxOutstandingPairCount - set correctly", async () => { - // // TODO: clean up key inputs like 20 into variables for easier readability and testing - // assert.equal(await bathHouseInstance.maxOutstandingPairCount(), 20); - // }); it("public address approvedPairContract - set correctly", async () => { assert.equal( await bathHouseInstance.approvedPairContract(), @@ -96,7 +91,6 @@ contract("Bath House", (accounts) => { ); }); it("public uint bpsToStrategists - set correctly", async () => { - //TODO: this is a hardcode rn assert.equal(await bathHouseInstance.bpsToStrategists(), 20); }); it("public mapping tokenToBathToken - initializes as empty before any createBathToken calls", async () => { @@ -105,32 +99,8 @@ contract("Bath House", (accounts) => { 0 ); }); - it("external onlyAdmin createBathToken - correctly spawns a new bathToken", async () => {}); - it("external onlyAdmin initBathPair - correctly initializes the sole bathPair contract", async () => {}); - it("external onlyAdmin setBathHouseAdmin - can change the admin of bathHouse securely", async () => {}); - it("external onlyAdmin setPermissionedStrategists - XXXX", async () => {}); - // *** Please note if any variables are unused in the v1 system *** - it("external onlyAdmin setReserveRatio - XXXX", async () => {}); - it("external onlyAdmin setCancelTimeDelay - XXXX", async () => {}); - it("external onlyAdmin setPropToStrats - XXX", async () => {}); - it("external onlyAdmin setMaxOutstandingPairCount - XXX", async () => {}); - it("external onlyAdmin setBathTokenMarket - XXX", async () => {}); - it("external onlyAdmin setBathTokenFeeBPS - XXX", async () => {}); - - it("external onlyAdmin setFeeTo - XXX", async () => {}); - it("external onlyAdmin setBathPairMOSBPS - XXX", async () => {}); - it("external onlyAdmin setBathPairSCN - XXX", async () => {}); - it("external onlyAdmin setMarket - XXX", async () => {}); - //IS this needed??? Check to make sure no getters for public variables - it("external view getMarket - XXX", async () => {}); - // check major variables only need getter if non public... - it("external view getBathTokenfromAsset - XXX", async () => {}); - it("external view isApprovedStrategist - XXX", async () => {}); - it("external view isApprovedPair - XXX", async () => {}); - it("external onlyAdmin approveStrategist - XXXX", async () => {}); }); describe("Case-Specific Tests", async function () { - it("public mapping approvedStrategists - can easily approve strategists", async () => {}); it("BathTokens can be created permissionlessly - openBathTokenSpawnAndSignal", async () => { // Will create a pool permissionlessly let signaler = accounts[1]; @@ -201,17 +171,5 @@ contract("Bath House", (accounts) => { ); should.equal(userBalance.split(".")[0], "100"); }); - it("BathTokens are correctly admin'd to _____ after createBathToken", async () => {}); - it("BathPairs are correctly admin'd to _____ after initialization via initBathPair", async () => {}); - it("public mapping tokenToBathToken - seamlessly maps an ERC-20 in the system to its bathToken", async () => {}); - it("** Key variables cannot be accessed by bad actors **", async () => {}); - }); - describe("Event Logging Tests", async function () { - // Coordinate with Subgraph on this - it("createBathToken is returning all needed and relevant data in an event", async () => {}); - it("initBathPair is returning all needed and relevant data in an event", async () => {}); - - // Want to make sure that actually used/important storage variables emit a log in their change function - // it("setAdmin emits a note on the new admin", async () => {}); // Is this needed...? }); }); diff --git a/test/BathPair_test.js b/test/BathPair_test.js index 653fbff..dec46db 100644 --- a/test/BathPair_test.js +++ b/test/BathPair_test.js @@ -94,17 +94,6 @@ contract("Bath Pair", (accounts) => { }); }); describe("Unit Tests", async function () { - it("public address bathHouse - set correctly", async () => {}); - it("public address RubiconMarketAddress - set correctly", async () => {}); - it("public bool initialized - correctly initialized", async () => {}); - // TODO: Need a getter? - it("internal int128 shapeCoefNum - correctly initialized", async () => {}); - //TODO: check this homie out - it("public uint maxOrderSizeBPS - correctly initialized", async () => {}); - // TODO: should this be internal? - - // ** Here we want to walk through each step of the full market-making flow while checking how key variables change ** - // 1. Place market making trades it("function placeMarketMakingTrades - a strategist can market-make for any ERC-20 pair", async () => { // WETH-DAI: 1: ($90, $110) @@ -137,44 +126,5 @@ contract("Bath Pair", (accounts) => { web3.utils.toWei(bidDen.toString()) ); }); - // 1a-1n make a test for each check along the way - - // 2. Simulate some fill on the pair of orders - // 2a-2n '' - it("function logFill( uint256 amt, address strategist, address asset) internal { - XXX", async () => {}); - - // 3. Scrub all orders (filled and otherwise) and verify that bathTokens and key variables are updated accordingly - - // 4. Rebalance the pools - it("function rebalancePair( uint256 assetRebalAmt, uint256 quoteRebalAmt, address _underlyingAsset, address _underlyingQuote) external onlyApprovedStrategist(msg.sender) {- XXX", async () => {}); - - // Check the following during the above: - it("public uint last_stratTrade_id - increments up correctly with a new order", async () => {}); - it("public mapping (address=>uint) totalFillsPerAsset - correctly logs an asset-specific fill for a strategist", async () => {}); - it("public mapping (uint=>StrategistTrade) strategistTrades - correctly adds in all *needed* info with a new strategist trade", async () => {}); - it("public mapping (address=>uint[]) outOffersByStrategist - correctly logs the outstanding array of orders a strategist has and removes scrubbed orders", async () => {}); - it("public mapping (address=>(address=>uint)) strategist2Fills - correctly adds in all *needed* info with a new strategist trade", async () => {}); - - // Functions to test for proper functionality GRAB BAG: - it("function getOutstandingTrades(address strategist) external view returns (uint256[] memory) - XXX", async () => {}); - it("function getMidpointPrice(address underlyingAsset, address underlyingQuote) internal view returns (int128) - XXX", async () => {}); - it("function handleStratOrderAtID( uint256 id, address bathAssetAddress, address bathQuoteAddress) internal { - XXX", async () => {}); - it(" ??? attempTail off??? Remove??? - XXX", async () => {}); - it("function scrubStrategistTrade(uint256 id,address bathAssetAddress,address bathQuoteAddress) public onlyApprovedStrategist(msg.sender) {- XXX", async () => {}); - it("function scrubStrategistTrades(uint256[] memory ids,address bathAssetAddress,address bathQuoteAddress) external { - XXX", async () => {}); - it("function getMaxOrderSize( address asset, address sisterAsset, address targetBathToken, address sisterBathToken) public view returns (uint256 maxSizeAllowed) { - XXX", async () => {}); - it("function strategistBootyClaim(address asset, address quote) external { - XXX", async () => {}); - }); - describe("Case-Specific Tests", async function () { - it("A strategist can place a bid OR an ask", async () => {}); - it("getMaxOrderSize scales correctly according to an understood shape curve", async () => {}); - it("Only approved strategists can submit orders and access user liquidity", async () => {}); - it("What is the maximum liquidity a strategist can utilize? Via what mechanism?", async () => {}); - it("The strategist can scrub multiple trades at once - log gas costs", async () => {}); - }); - describe("Event Logging Tests", async function () { - it("Any new strategist trade emits an event with the data we need", async () => {}); - it("A rebalance emits data correctly", async () => {}); - it("A scrubbing of an outstanding order ?needed? emits data correctly", async () => {}); }); }); diff --git a/test/BathToken_test.js b/test/BathToken_test.js index 0764203..a11bb7c 100644 --- a/test/BathToken_test.js +++ b/test/BathToken_test.js @@ -13,14 +13,6 @@ function logIndented(...args) { const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; -//Special attention to: -// - Lossless outstanding amount in bathTokens - closed loop! -// - Permissions and potential exploits -// - TODO: need to add ***** arbitrary bathToken reward ERC-20s ** - -//Edge Case: -// - oustanding amount minor change - is this relevant? How does it affect mint ratio? - contract("Bath Token", (accounts) => { let rubiconMarketInstance; let bathHouseInstance; @@ -39,7 +31,6 @@ contract("Bath Token", (accounts) => { DAIInstance = await DAI.deployed(); WETHInstance = await WETH.deployed(); bathTokenImplementation = await BathToken.new(); - }); it("Is successfully initialized", async () => { await bathHouseInstance.initialize( @@ -77,63 +68,12 @@ contract("Bath Token", (accounts) => { let newBathToken = await bathHouseInstance.tokenToBathToken( newCoin.address ); - // logIndented("Getting this new bathToken", newBathToken); let bathToken = await BathToken.at(newBathToken); let bathTokenName = await bathToken.name(); let bathTokenSymbol = await bathToken.symbol(); const expectedBathTokenName = "bath" + newCoinSymbol; assert.equal(bathTokenSymbol, expectedBathTokenName); assert.equal(bathTokenName, expectedBathTokenName + " v1"); - - // logIndented("name", bathTokenName); - // logIndented("symbol", bathTokenSymbol); }); - it("BathTokens are successfully initialized whenever they are created", async () => {}); - }); - describe("Unit Tests", async function () { - it("bool public initialized; - XXX", async () => {}); - it("string public symbol; - set correctly", async () => {}); - it("string public name; - set correctly", async () => {}); - it("uint8 public decimals; - set correctly", async () => {}); - it("address public RubiconMarketAddress; - set correctly", async () => {}); - it("address public bathHouse; - set correctly", async () => {}); - it("address public feeTo; - set correctly", async () => {}); - it("IERC20 public underlyingToken; - set correctly!", async () => {}); - // TODO: verify the way in which these feeBPS map around... - it("uint256 public feeBPS; - set correctly", async () => {}); - it("uint256 public totalSupply; - increments correctly with new deposits", async () => {}); - it("uint256 public outstandingAmount; - losslessly tracks outstanding liquidity", async () => {}); - it("*** go for the Permit variables to verify ***??", async () => {}); - it("function setMarket(address newRubiconMarket) external { - works as expected", async () => {}); - it("function setBathHouse(address newBathHouse) external { - works as expected", async () => {}); - it("function setFeeBPS(uint256 _feeBPS) external { - works as expected", async () => {}); - it("function setFeeTo(address _feeTo) external { - works as expected", async () => {}); - it("function underlying() external view returns (address) { - works as expected", async () => {}); - it("function underlyingBalance() public view returns (uint256) { - works as expected", async () => {}); - it("function cancel(uint256 id, uint256 amt) external onlyPair { - works as expected", async () => {}); - it("function removeFilledTradeAmount(uint256 amt) external onlyPair { - works as expected", async () => {}); - it("function placeOffer( uint256 pay_amt, ERC20 pay_gem, uint256 buy_amt, ERC20 buy_gem) external onlyPair returns (uint256) { - works as expected", async () => {}); - it("function rebalance( address sisterBath, address underlyingAsset, /* sister asset */ uint256 stratProportion, uint256 rebalAmt) external onlyPair { - works as expected", async () => {}); - it("function deposit(uint256 _amount) external returns (uint256 shares) { - works as expected", async () => {}); - it("function withdraw(uint256 _shares) external returns (uint256 amountWithdrawn) { - works as expected", async () => {}); - // Make requiring initialization a modifier or assume it ? just revisit - it("function approveMarket() external {", async () => {}); - }); - describe("Case-Specific Tests", async function () { - it("A user is minted the right amount of shares when depositing", async () => {}); - it("Utilization/outstanding orders cause none (or minor?) variation in the mint/withdraw ratio", async () => {}); - it("A user withdraws assets at the correct ratio", async () => {}); - it("A strategist cannot exceed the reserve ratio of this pool ? TODO/Revisit", async () => {}); - it("Arbitrary tokens can be earned as yield and passed to shareholders", async () => {}); - it("In what situation can the share model or yield accrual be exploited?", async () => {}); - it("Ensure that permissionless pool creation is possible though onlyApproved strategists can do anything with ERC-20 liquidity", async () => {}); - }); - describe("Event Logging Tests", async function () { - it("ALL ERC-20 actions capture an event", async () => {}); - it("All deposits capture all relevant info and some added data for performance tracking", async () => {}); - it("All withdraws capture all relevant info and some added data for performance tracking", async () => {}); - it("Any time funds are removed from the pool we know about it", async () => {}); - it("Any time funds are returned to the pool we know about it", async () => {}); - it("Any time a bathToken is spawned we capture all needed info in an event", async () => {}); // could be bathhouse }); }); diff --git a/test/RubiconMarket_test.js b/test/RubiconMarket_test.js deleted file mode 100644 index bbb597d..0000000 --- a/test/RubiconMarket_test.js +++ /dev/null @@ -1,42 +0,0 @@ -const RubiconMarket = artifacts.require("RubiconMarket"); -const DAI = artifacts.require("TokenWithFaucet"); -const WETH = artifacts.require("WETH9"); - -//Helper function -function logIndented(...args) { - console.log(" ", ...args); -} - -// Goal here will be to focus tests on any added functionality as the core logic is somewhat lindy... -// This one will require a lot of iterative work - -contract("Rubicon Market", (accounts) => { - let rubiconMarketInstance; - let bathHouseInstance; - let bathPairInstance; - let bathAssetInstance; - let bathQuoteInstance; - let DAIInstance; - let WETHInstance; - - describe("Deployment & Startup", async function () { - it("Is deployed successfully", async () => { - rubiconMarketInstance = await RubiconMarket.deployed(); - DAIInstance = await DAI.deployed(); - WETHInstance = await WETH.deployed(); - }); - }); - describe("Unit Tests", async function () { - it("", async () => {}); - }); - describe("Case-Specific Tests", async function () { - it("Fees are correctly accrued to the fee recipient", async () => {}); - it("** Do some work to understand matching behavior better **", async () => {}); - }); - describe("Event Logging Tests", async function () { - it("LogTake", async () => {}); - it("LogMake", async () => {}); - it("LogKill", async () => {}); - it("... add in key events ...", async () => {}); - }); -}); diff --git a/test/RubiconRouter_test.js b/test/RubiconRouter_test.js index 616fbf6..a4a2eb1 100644 --- a/test/RubiconRouter_test.js +++ b/test/RubiconRouter_test.js @@ -195,24 +195,25 @@ contract("Rubicon Router", (accounts) => { assert.equal(bids[1][2].toNumber(), 4); assert.equal(bids[2][2].toNumber(), 6); }); - it("function getBestOfferAndInfo(address asset, address quote) public view returns ( uint256, //id uint256, ERC20, uint256, ERC20)", async () => {}); - it("function approveAssetOnMarket(address toApprove) public {", async () => {}); //right security in place? - it("function getExpectedSwapFill()", async () => {}); - it("function swap() public returns (uint) **", async () => {}); - it("function _swap( uint256 pay_amt, uint256 buy_amt_min, address[] calldata route, uint256 expectedMarketFeeBPS, address to) internal returns (uint256) {", async () => {}); - - // Are these foolish to have in? Is this a dust solution that makes sense? - it("function swapEntireBalance( uint256 buy_amt_min, address[] calldata route, // First address is what is being payed, Last address is what is being bought uint256 expectedMarketFeeBPS) external returns (uint256) {", async () => {}); - it("function maxBuyAllAmount( ERC20 buy_gem, ERC20 pay_gem, uint256 max_fill_amount) external returns (uint256 fill) {", async () => {}); - it("function maxSellAllAmount( ERC20 pay_gem, ERC20 buy_gem, uint256 min_fill_amount) external returns (uint256 fill) {", async () => {}); - }); + }); describe("Case-Specific Tests", async function () { it("Random: ERC-20 Token with faucet behaves as expected", async () => { let decimals = 18; - let newTWF = await TokenWithFaucet.new(accounts[3], "Test Coin", "TEST", decimals); - assert.equal(await (await newTWF).balanceOf(accounts[3]), 1000 * (10 ** decimals)); - await (await newTWF).faucet({from: accounts[3]}); - assert.equal(await (await newTWF).balanceOf(accounts[3]), 2 * (1000 * (10 ** decimals))); + let newTWF = await TokenWithFaucet.new( + accounts[3], + "Test Coin", + "TEST", + decimals + ); + assert.equal( + await (await newTWF).balanceOf(accounts[3]), + 1000 * 10 ** decimals + ); + await (await newTWF).faucet({ from: accounts[3] }); + assert.equal( + await (await newTWF).balanceOf(accounts[3]), + 2 * (1000 * 10 ** decimals) + ); }); // Approach: // Have a user with native ETH, track their balances of everything before and after @@ -335,6 +336,7 @@ contract("Rubicon Router", (accounts) => { from: accounts[0], } ); + // Offer and expect to fill in native ETH await rubiconRouterInstance.offerForETH( web3.utils.toWei("1"), @@ -345,26 +347,38 @@ contract("Rubicon Router", (accounts) => { ); let ethBalanceAfter = await web3.eth.getBalance(accounts[0]); let delta = ethBalanceAfter - ethBalanceBefore; - assert.isAtLeast(delta, parseInt(await web3.utils.toWei("0.00046"))); + assert.isAtLeast(delta, parseInt(await web3.utils.toWei("0.00099"))); }); it("[Native ETH] - A user can buyAllAmount with native ETH", async () => { - let ethBalanceBefore = await web3.eth.getBalance(accounts[1]); + // let ethBalanceBefore = await web3.eth.getBalance(accounts[1]); + const max_fill_amount = web3.utils.toWei("0.1"); - let buyAmt = await rubiconMarketInstance.getBuyAmount(DAIInstance.address, WETHInstance.address, web3.utils.toWei("0.1")); - let payAmt = await rubiconMarketInstance.getPayAmount( WETHInstance.address,DAIInstance.address, web3.utils.toWei("9")); // This is equal to what resulting dai we get... + let buyAmt = await rubiconMarketInstance.getBuyAmount( + DAIInstance.address, + WETHInstance.address, + max_fill_amount + ); + let payAmt = await rubiconMarketInstance.getPayAmount( + WETHInstance.address, + DAIInstance.address, + web3.utils.toWei("9") + ); // This is equal to what resulting dai we get... // logIndented("Expected payAmt", payAmt.toString()); await rubiconRouterInstance.buyAllAmountWithETH( DAIInstance.address, - web3.utils.toWei("9"), - web3.utils.toWei("0.1"), + buyAmt, + max_fill_amount, 20, { from: accounts[1], value: web3.utils.toWei("0.1002") } - ); - let ethBalanceAfter = await web3.eth.getBalance(accounts[1]); - let delta = ethBalanceBefore - ethBalanceAfter; + ); + // let ethBalanceAfter = await web3.eth.getBalance(accounts[1]); + // let delta = ethBalanceBefore - ethBalanceAfter; // logIndented("spent this much eth", web3.utils.fromWei(delta.toString())); - assert.equal((await DAIInstance.balanceOf(accounts[1])).toString(), buyAmt.toString()); + assert.equal( + (await DAIInstance.balanceOf(accounts[1])).toString(), + buyAmt.toString() + ); // assert.isAtLeast(delta, parseInt(await web3.utils.toWei("0.1"))); }); it("[Native ETH] - A user can buyAllAmount for native ETH", async () => { @@ -380,29 +394,22 @@ contract("Rubicon Router", (accounts) => { //Inputting fee reduced amount vs await rubiconRouterInstance.buyAllAmountForETH( await web3.utils.toWei("0.0892"), - DAIInstance.address, + DAIInstance.address, web3.utils.toWei("8.982"), // 99.8% to account for fee { from: accounts[0] } ); let ethBalanceAfter = await web3.eth.getBalance(accounts[0]); let delta = ethBalanceAfter - ethBalanceBefore; - assert.isAtLeast(delta, parseInt(await web3.utils.toWei("0.088"))); + assert.isAtLeast(delta, parseInt(await web3.utils.toWei("0.0891999"))); }); it("[Native ETH] - A user can cancel for native ETH", async () => { //admin cancels id 7 and expects native eth let ethBalanceBefore = await web3.eth.getBalance(accounts[6]); //Inputting fee reduced amount vs - await rubiconRouterInstance.cancelForETH( - 7, - { from: accounts[6] } - ); + await rubiconRouterInstance.cancelForETH(7, { from: accounts[6] }); let ethBalanceAfter = await web3.eth.getBalance(accounts[6]); let delta = ethBalanceAfter - ethBalanceBefore; - assert.isAtLeast(delta, parseInt(await web3.utils.toWei("0.099"))); - }); - }); - describe("Event Logging Tests", async function () { - it("We are logging swaps so can track % of trades that are swaps", async () => {}); - // ANy more? + assert.isAtLeast(delta, parseInt(await web3.utils.toWei("0.099999"))); + }); }); }); diff --git a/test/oldTests/1_protocol_test.js b/test/oldTests/1_protocol_test.js index ff3ee13..3dda08d 100644 --- a/test/oldTests/1_protocol_test.js +++ b/test/oldTests/1_protocol_test.js @@ -6,6 +6,7 @@ const DAI = artifacts.require("TokenWithFaucet"); const WETH = artifacts.require("WETH9"); const TokenWithFaucet = artifacts.require("TokenWithFaucet"); +const { parseUnits } = require("ethers/lib/utils.js"); const helper = require("../testHelpers/timeHelper.js"); function logIndented(...args) { @@ -15,249 +16,244 @@ function logIndented(...args) { // ganache-cli --gasLimit=0x1fffffffffffff --gasPrice=0x1 --allowUnlimitedContractSize --defaultBalanceEther 9000 // ganache-cli --gasLimit=9000000 --gasPrice=0x1 --defaultBalanceEther 9000 --allowUnlimitedContractSize -contract("Rubicon Market and Pools Original Tests", async function (accounts) { - let newPair; - let bathPairInstance; - let bathAssetInstance; - let bathQuoteInstance; - let bathHouseInstance; - let bathTokenImplementation; - - describe("Deployment", async function () { - it("is deployed", async function () { - rubiconMarketInstance = await RubiconMarket.deployed(); - bathHouseInstance = await BathHouse.deployed(); - DAIInstance = await DAI.deployed(); - WETHInstance = await WETH.deployed(); - bathPairInstance = await BathPair.deployed(); - bathTokenImplementation = await BathToken.new(); +contract( + "Rubicon Exchange and Pools Original Tests", + async function (accounts) { + let newPair; + let bathPairInstance; + let bathAssetInstance; + let bathQuoteInstance; + let bathHouseInstance; + let bathTokenImplementation; + + describe("Deployment", async function () { + it("is deployed", async function () { + rubiconMarketInstance = await RubiconMarket.deployed(); + bathHouseInstance = await BathHouse.deployed(); + DAIInstance = await DAI.deployed(); + WETHInstance = await WETH.deployed(); + bathPairInstance = await BathPair.deployed(); + bathTokenImplementation = await BathToken.new(); + }); }); - }); - describe("Bath House Initialization of Bath Pair and Bath Tokens", async function () { - it("Bath House is deployed and initialized", async function () { - // Call initialize on Bath house - return await bathHouseInstance.initialize( - rubiconMarketInstance.address, - 80, - 10, - bathTokenImplementation.address, - accounts[9] // Proxy admin - // 20 - ); - }); + describe("Bath House Initialization of Bath Pair and Bath Tokens", async function () { + it("Bath House is deployed and initialized", async function () { + // Call initialize on Bath house + return await bathHouseInstance.initialize( + rubiconMarketInstance.address, + 80, + 10, + bathTokenImplementation.address, + accounts[9] // Proxy admin + // 20 + ); + }); - it("Bath Token for asset is deployed and initialized", async function () { - await bathHouseInstance.createBathToken( - WETHInstance.address, - accounts[0] - ); - let newBathTokenAddr = await bathHouseInstance.tokenToBathToken( - WETHInstance.address - ); - logIndented("new bathWETH!", newBathTokenAddr.toString()); - bathAssetInstance = await BathToken.at(newBathTokenAddr); - assert.equal(await bathAssetInstance.RubiconMarketAddress(), rubiconMarketInstance.address); - }); - it("Bath Token for quote is deployed and initialized", async function () { - await bathHouseInstance.createBathToken( - DAIInstance.address, - accounts[0] - ); - let newBathTokenAddr = await bathHouseInstance.tokenToBathToken( - DAIInstance.address - ); - // logIndented("new addr!",await bathHouseInstance.tokenToBathToken(WETHInstance.address)); - bathQuoteInstance = await BathToken.at(newBathTokenAddr); - assert.equal(await bathQuoteInstance.RubiconMarketAddress(), rubiconMarketInstance.address); - }); - // Now is initialized from the BathHouse itself - it("Bath Pair is deployed and initialized w/ BathHouse", async function () { - await bathHouseInstance.initBathPair(bathPairInstance.address, 500, -5); // 90% reserve ratio and 3 days cancel delay - livePair = await bathHouseInstance.approvedPairContract(); - logIndented("New BathPair: ", newPair); - assert.equal(livePair.toString(), bathPairInstance.address); - }); - it("can correctly spawn bathWETH and bathDAI", async function () { - let assetName = await bathAssetInstance.symbol(); - let quoteName = await bathQuoteInstance.symbol(); - assert.equal(assetName, "bathWETH"); - assert.equal(quoteName, "bathDAI"); - }); - it("bath tokens have the right name", async function () { - assert.equal(await bathAssetInstance.symbol(), "bathWETH"); - assert.equal(await bathQuoteInstance.symbol(), "bathDAI"); - }); - it("User can deposit asset funds with custom weights and receive bathTokens", async function () { - await WETHInstance.deposit({ - from: accounts[1], - value: web3.utils.toWei((1).toString()), + it("Bath Token for asset is deployed and initialized", async function () { + await bathHouseInstance.createBathToken( + WETHInstance.address, + accounts[0] + ); + let newBathTokenAddr = await bathHouseInstance.tokenToBathToken( + WETHInstance.address + ); + logIndented("new bathWETH!", newBathTokenAddr.toString()); + bathAssetInstance = await BathToken.at(newBathTokenAddr); + assert.equal( + await bathAssetInstance.RubiconMarketAddress(), + rubiconMarketInstance.address + ); }); - await WETHInstance.approve( - bathAssetInstance.address, - web3.utils.toWei((1).toString()), - { from: accounts[1] } - ); - await bathAssetInstance.deposit(web3.utils.toWei((1).toString()), { - from: accounts[1], + it("Bath Token for quote is deployed and initialized", async function () { + await bathHouseInstance.createBathToken( + DAIInstance.address, + accounts[0] + ); + let newBathTokenAddr = await bathHouseInstance.tokenToBathToken( + DAIInstance.address + ); + // logIndented("new addr!",await bathHouseInstance.tokenToBathToken(WETHInstance.address)); + bathQuoteInstance = await BathToken.at(newBathTokenAddr); + assert.equal( + await bathQuoteInstance.RubiconMarketAddress(), + rubiconMarketInstance.address + ); }); - assert.equal( - (await bathAssetInstance.balanceOf(accounts[1])).toString(), - web3.utils.toWei((1).toString()) - ); - }); - it("User can deposit quote funds with custom weights and receive bathTokens", async function () { - await DAIInstance.faucet({ from: accounts[2] }); - await DAIInstance.approve( - bathQuoteInstance.address, - web3.utils.toWei((100).toString()), - { from: accounts[2] } - ); - await bathQuoteInstance.deposit(web3.utils.toWei((100).toString()), { - from: accounts[2], + // Now is initialized from the BathHouse itself + it("Bath Pair is deployed and initialized w/ BathHouse", async function () { + await bathHouseInstance.initBathPair(bathPairInstance.address, 500, -5); // 90% reserve ratio and 3 days cancel delay + livePair = await bathHouseInstance.approvedPairContract(); + logIndented("New BathPair: ", newPair); + assert.equal(livePair.toString(), bathPairInstance.address); }); - assert.equal( - (await bathQuoteInstance.balanceOf(accounts[2])).toString(), - web3.utils.toWei((100).toString()) - ); - }); - it("Withdraw asset funds by sending in bathTokens", async function () { - await bathAssetInstance.withdraw(web3.utils.toWei((1).toString()), { - from: accounts[1], + it("can correctly spawn bathWETH and bathDAI", async function () { + let assetName = await bathAssetInstance.symbol(); + let quoteName = await bathQuoteInstance.symbol(); + assert.equal(assetName, "bathWETH"); + assert.equal(quoteName, "bathDAI"); }); - assert.equal( - await WETHInstance.balanceOf(accounts[1]), - web3.utils.toWei((1).toString()) - ); - }); - it("Withdraw quote funds by sending in bathTokens", async function () { - await bathQuoteInstance.withdraw(web3.utils.toWei((100).toString()), { - from: accounts[2], + it("bath tokens have the right name", async function () { + assert.equal(await bathAssetInstance.symbol(), "bathWETH"); + assert.equal(await bathQuoteInstance.symbol(), "bathDAI"); }); - assert.equal( - (await DAIInstance.balanceOf(accounts[2])).toString(), - web3.utils.toWei("1000").toString() - ); - }); - it("both users have no bath Tokens post withdraw", async function () { - assert.equal("0", await bathAssetInstance.balanceOf(accounts[1])); - assert.equal("0", await bathQuoteInstance.balanceOf(accounts[2])); - }); - }); - - // Test Market making functionality: - describe("Liquidity Providing Tests", async function () { - // Bid and ask made by Pools throughout the test - const askNumerator = web3.utils.toWei((0.01).toString()); - const askDenominator = web3.utils.toWei((0.5).toString()); - const bidNumerator = web3.utils.toWei((0.4).toString()); - const bidDenominator = web3.utils.toWei((0.01).toString()); + it("User can deposit asset funds with custom weights and receive bathTokens", async function () { + await WETHInstance.deposit({ + from: accounts[1], + value: web3.utils.toWei((1).toString()), + }); + await WETHInstance.approve( + bathAssetInstance.address, + web3.utils.toWei((1).toString()), + { from: accounts[1] } + ); + logIndented(bathAssetInstance.functions); + await bathAssetInstance.methods["deposit(uint256)"]( + web3.utils.toWei((1).toString()), + { from: accounts[1] } + ); - it("User can deposit asset funds with custom weights and receive bathTokens", async function () { - await WETHInstance.deposit({ - from: accounts[1], - value: web3.utils.toWei((10).toString()), + assert.equal( + (await bathAssetInstance.balanceOf(accounts[1])).toString(), + web3.utils.toWei((1).toString()) + ); }); - await WETHInstance.approve( - bathAssetInstance.address, - web3.utils.toWei((10).toString()), - { from: accounts[1] } - ); - - await bathAssetInstance.deposit(web3.utils.toWei((10).toString()), { - from: accounts[1], + it("User can deposit quote funds with custom weights and receive bathTokens", async function () { + // Faucets 1000 + await DAIInstance.faucet({ from: accounts[2] }); + await DAIInstance.approve( + bathQuoteInstance.address, + web3.utils.toWei((100).toString()), + { from: accounts[2] } + ); + await bathQuoteInstance.methods["deposit(uint256)"]( + web3.utils.toWei((100).toString()), + { + from: accounts[2], + } + ); + assert.equal( + (await bathQuoteInstance.balanceOf(accounts[2])).toString(), + web3.utils.toWei((100).toString()) + ); }); - assert.equal( - (await bathAssetInstance.balanceOf(accounts[1])).toString(), - web3.utils.toWei((10).toString()) - ); - }); - it("Users can deposit quote funds with custom weights and receive bathTokens", async function () { - await DAIInstance.faucet({ from: accounts[2] }); - await DAIInstance.approve( - bathQuoteInstance.address, - web3.utils.toWei((100).toString()), - { from: accounts[2] } - ); - - await bathQuoteInstance.deposit(web3.utils.toWei((100).toString()), { - from: accounts[2], + it("Withdraw asset funds by sending in bathTokens", async function () { + const shares = 1; + await bathAssetInstance.withdraw(web3.utils.toWei((shares).toString()), { + from: accounts[1], + }); + // Account for fee + // const expected = parseInt((shares * 10000) - ((shares) * (10000) / (3))); + assert.equal( + (await WETHInstance.balanceOf(accounts[1])).toString(), + await web3.utils.toWei((shares - 0.0003 * shares).toString()).toString() + ); + }); + it("Withdraw quote funds by sending in bathTokens", async function () { + const shares = 100; + await bathQuoteInstance.withdraw(web3.utils.toWei((shares).toString()), { + from: accounts[2], + }); + + // Account for fee + // const expected = parseInt((shares * 10000) - ((shares) * (10000) / (3))); + assert.equal( + (await DAIInstance.balanceOf(accounts[2])).toString(), + web3.utils.toWei(((shares - 0.0003 * shares) + 900).toString()).toString() + ); + }); + it("both users have no bath Tokens post withdraw", async function () { + assert.equal("0", await bathAssetInstance.balanceOf(accounts[1])); + assert.equal("0", await bathQuoteInstance.balanceOf(accounts[2])); }); - assert.equal( - (await bathQuoteInstance.balanceOf(accounts[2])).toString(), - web3.utils.toWei((100).toString()) - ); }); - it("Place a starting pair to clear checks", async function () { - await WETHInstance.deposit({ - from: accounts[3], - value: web3.utils.toWei((0.5).toString()), + + // Test Market making functionality: + describe("Liquidity Providing Tests", async function () { + // Bid and ask made by Pools throughout the test + const askNumerator = web3.utils.toWei((0.01).toString()); + const askDenominator = web3.utils.toWei((0.5).toString()); + const bidNumerator = web3.utils.toWei((0.4).toString()); + const bidDenominator = web3.utils.toWei((0.01).toString()); + + it("User can deposit asset funds with custom weights and receive bathTokens", async function () { + await WETHInstance.deposit({ + from: accounts[1], + value: web3.utils.toWei((10).toString()), + }); + await WETHInstance.approve( + bathAssetInstance.address, + web3.utils.toWei((10).toString()), + { from: accounts[1] } + ); + + await bathAssetInstance.methods["deposit(uint256)"]( + web3.utils.toWei((10).toString()), + { + from: accounts[1], + } + ); + assert.equal( + (await bathAssetInstance.balanceOf(accounts[1])).toString(), + web3.utils.toWei((10).toString()) + ); }); - await WETHInstance.approve( - rubiconMarketInstance.address, - web3.utils.toWei((0.5).toString()), - { from: accounts[3] } - ); - await rubiconMarketInstance.offer( - web3.utils.toWei((0.1).toString(), "ether"), - WETHInstance.address, - web3.utils.toWei((5).toString(), "ether"), - DAIInstance.address, - 0, - { from: accounts[3] } - ); + it("Users can deposit quote funds with custom weights and receive bathTokens", async function () { + await DAIInstance.faucet({ from: accounts[2] }); + await DAIInstance.approve( + bathQuoteInstance.address, + web3.utils.toWei((100).toString()), + { from: accounts[2] } + ); - // To trigger faucet again: - // helper.advanceTimeAndBlock(8700); - await DAIInstance.faucet({ from: accounts[4] }); - await DAIInstance.approve( - rubiconMarketInstance.address, - web3.utils.toWei((70).toString()), - { from: accounts[4] } - ); - await rubiconMarketInstance.offer( - web3.utils.toWei((4).toString(), "ether"), - DAIInstance.address, - web3.utils.toWei((0.1).toString(), "ether"), - WETHInstance.address, - 0, - { from: accounts[4], gas: 0x1ffffff } - ); - }); - it("placeMarketMaking Trades can be called by approved strategist successfully", async function () { - await bathPairInstance.placeMarketMakingTrades( - [WETHInstance.address, DAIInstance.address], - askNumerator, - askDenominator, - bidNumerator, - bidDenominator - ); - }); - it("bathTokens maintains the correct underlyingBalance()", async function () { - assert.equal( - (await bathAssetInstance.underlyingBalance()).toString(), - web3.utils.toWei((10).toString()) - ); - }); - it("Taker can fill part of trade", async function () { - await WETHInstance.deposit({ - from: accounts[5], - value: web3.utils.toWei((100).toString()), + await bathQuoteInstance.methods["deposit(uint256)"]( + web3.utils.toWei((100).toString()), + { + from: accounts[2], + } + ); + assert.equal( + (await bathQuoteInstance.balanceOf(accounts[2])).toString(), + web3.utils.toWei((100).toString()) + ); }); - await WETHInstance.approve( - rubiconMarketInstance.address, - web3.utils.toWei((100).toString()), - { from: accounts[5] } - ); + it("Place a starting pair to clear checks", async function () { + await WETHInstance.deposit({ + from: accounts[3], + value: web3.utils.toWei((0.5).toString()), + }); + await WETHInstance.approve( + rubiconMarketInstance.address, + web3.utils.toWei((0.5).toString()), + { from: accounts[3] } + ); + await rubiconMarketInstance.offer( + web3.utils.toWei((0.1).toString(), "ether"), + WETHInstance.address, + web3.utils.toWei((5).toString(), "ether"), + DAIInstance.address, + 0, + { from: accounts[3] } + ); - await rubiconMarketInstance.buy(4, web3.utils.toWei((0.4).toString()), { - from: accounts[5], + // To trigger faucet again: + // helper.advanceTimeAndBlock(8700); + await DAIInstance.faucet({ from: accounts[4] }); + await DAIInstance.approve( + rubiconMarketInstance.address, + web3.utils.toWei((70).toString()), + { from: accounts[4] } + ); + await rubiconMarketInstance.offer( + web3.utils.toWei((4).toString(), "ether"), + DAIInstance.address, + web3.utils.toWei((0.1).toString(), "ether"), + WETHInstance.address, + 0, + { from: accounts[4], gas: 0x1ffffff } + ); }); - }); - it("scrubStrategistTrades can be used by approved strategists", async function () { - let target = 2; - // let goalScrub = target + outCount.toNumber(); - for (let index = 0; index < target; index++) { + it("placeMarketMaking Trades can be called by approved strategist successfully", async function () { await bathPairInstance.placeMarketMakingTrades( [WETHInstance.address, DAIInstance.address], askNumerator, @@ -265,33 +261,92 @@ contract("Rubicon Market and Pools Original Tests", async function (accounts) { bidNumerator, bidDenominator ); - // await rubiconMarketInstance.buy(4 + (2 * (index + 1)), web3.utils.toWei((0.4).toString()), { - // from: accounts[5], - // }); - } - // const outCount = await bathPairInstance.getOutstandingStrategistTrades(accounts[0]); - // logIndented("outTrades", outCount); - // logIndented("outstanding trades count!", outCount.length); - - // logIndented( - // "cost of indexScrub:", - // await bathPairInstance.indexScrub.estimateGas(0, 2) - // ); - // TODO: gas considerations? - // await bathPairInstance.scrubStrategistTrades([outCount[0], outCount[1]]); + }); + it("bathTokens maintains the correct underlyingBalance()", async function () { + assert.equal( + (await bathAssetInstance.underlyingBalance()).toString(), + web3.utils.toWei((10 + 0.0003).toString()) + ); + }); + it("Taker can fill part of trade", async function () { + await WETHInstance.deposit({ + from: accounts[5], + value: web3.utils.toWei((100).toString()), + }); + await WETHInstance.approve( + rubiconMarketInstance.address, + web3.utils.toWei((100).toString()), + { from: accounts[5] } + ); - // helper.advanceTimeAndBlock(100); - //TODO: make this test actually work - assert.equal( - ( - await bathPairInstance.getOutstandingStrategistTrades(WETHInstance.address, DAIInstance.address, accounts[0]) - ).length.toString(), - "3" - ); - }); - it("bathTokens are correctly logging outstandingAmount", async function () { - let target = 6; - for (let index = 0; index < target; index++) { + await rubiconMarketInstance.buy(4, web3.utils.toWei((0.4).toString()), { + from: accounts[5], + }); + }); + it("scrubStrategistTrades can be used by approved strategists", async function () { + let target = 2; + // let goalScrub = target + outCount.toNumber(); + for (let index = 0; index < target; index++) { + await bathPairInstance.placeMarketMakingTrades( + [WETHInstance.address, DAIInstance.address], + askNumerator, + askDenominator, + bidNumerator, + bidDenominator + ); + } + assert.equal( + ( + await bathPairInstance.getOutstandingStrategistTrades( + WETHInstance.address, + DAIInstance.address, + accounts[0] + ) + ).length.toString(), + "3" + ); + }); + it("bathTokens are correctly logging outstandingAmount", async function () { + let target = 6; + for (let index = 0; index < target; index++) { + await bathPairInstance.placeMarketMakingTrades( + [WETHInstance.address, DAIInstance.address], + askNumerator, + askDenominator, + bidNumerator, + bidDenominator + ); + } + helper.advanceTimeAndBlock(100); + // Wipe the book of strategist trades! + const outCount = await bathPairInstance.getOutstandingStrategistTrades( + WETHInstance.address, + DAIInstance.address, + accounts[0] + ); + for (let index = 0; index < outCount.length; index++) { + const element = outCount[index]; + // logIndented("Attempting to scrub this id:", element.toNumber()); + await bathPairInstance.scrubStrategistTrade(await element.toNumber()); + helper.advanceTimeAndBlock(1); + } + + assert.equal( + (await bathAssetInstance.outstandingAmount()).toString(), + "0" + ); + assert.equal( + (await bathQuoteInstance.outstandingAmount()).toString(), + "0" + ); + }); + it("One cannot scrub already scrubbed orders", async function () { + await bathPairInstance.scrubStrategistTrade(1).catch((e) => { + logIndented("Should be an error reason:", e.reason); + }); + }); + it("Can placeMarketMakingTrades", async function () { + // await bathPairInstance.bathScrub(); await bathPairInstance.placeMarketMakingTrades( [WETHInstance.address, DAIInstance.address], askNumerator, @@ -299,277 +354,154 @@ contract("Rubicon Market and Pools Original Tests", async function (accounts) { bidNumerator, bidDenominator ); - } - helper.advanceTimeAndBlock(100); - // Wipe the book of strategist trades! - const outCount = await bathPairInstance.getOutstandingStrategistTrades(WETHInstance.address, DAIInstance.address,accounts[0]); - // logIndented("** getting this out **", outCount.toString()); - // logIndented("WETH", await WETHInstance.address); - // logIndented("is this bathAsset?", await bathHouseInstance.getBathTokenfromAsset(WETHInstance.address)); - // logIndented("outcount length", outCount.length); - // logIndented("outcount is this", outCount); - - // This is reverting: - // 3 is working here... - for (let index = 0; index < outCount.length; index++) { - const element = outCount[index]; - // logIndented("Attempting to scrub this id:", element.toNumber()); - await bathPairInstance.scrubStrategistTrade(await element.toNumber()); - helper.advanceTimeAndBlock(1); - } - - assert.equal( - (await bathAssetInstance.outstandingAmount()).toString(), - "0" - ); - assert.equal( - (await bathQuoteInstance.outstandingAmount()).toString(), - "0" - ); - }); - it("Partial fill is correctly cancelled and replaced", async function () { - // await bathPairInstance.bathScrub(); - //TODO: make this actually good. This test is not working rn - - await bathPairInstance.placeMarketMakingTrades( - [WETHInstance.address, DAIInstance.address], - askNumerator, - askDenominator, - bidNumerator, - bidDenominator - ); - }); - it("Zero order can be placed - bid or ask", async function () { - await bathPairInstance.placeMarketMakingTrades( - [WETHInstance.address, DAIInstance.address], - 0, - 0, - bidNumerator, - bidDenominator - ); - await bathPairInstance.placeMarketMakingTrades( - [WETHInstance.address, DAIInstance.address], - askNumerator, - askDenominator, - 0, - 0 - ); - }); - // Works just not needed in current strategist flow - // it("Strategist can cancel an order they made", async function () { - // logIndented( - // "cost of remove liqudity:", - // await bathPairInstance.removeLiquidity.estimateGas(7) - // ); - // await bathPairInstance.removeLiquidity(7); - // // assert.equal((await bathPairInstance.getOutstandingPairCount()).toString(), "2"); - // }); - it("New strategist can be added to pools ", async function () { - await bathHouseInstance.approveStrategist(accounts[6]); - await bathPairInstance.placeMarketMakingTrades( - [WETHInstance.address, DAIInstance.address], - askNumerator, - askDenominator, - bidNumerator, - bidDenominator, - { from: accounts[6] } - ); - // await bathPairInstance.removeLiquidity(10, {from: accounts[6]}); - }); - // for (let i = 1; i < 10; i++) { - // it(`Spamming of placeMarketMakingTrades iteration: ${i}`, async function () { - // await bathPairInstance.placeMarketMakingTrades( askNumerator, askDenominator, bidNumerator, bidDenominator); - // // TODO: log gas while looping through multiple bathScrub calls - // // See how it scales and if a solution is available to make it more gas efficient - // // --> why in the OVM is bathScrub failing? This is the goal... - - // await rubiconMarketInstance.buy(8 + (i*2), web3.utils.toWei((0.4).toString()), { from: accounts[5] }); - // // console.log(await bathPairInstance.placeMarketMakingTrades.estimateGas( askNumerator, askDenominator, bidNumerator, bidDenominator)); - // // console.log("IDs of new trades: ", await bathPairInstance.getLastTradeIDs()); - // let outstandingPairs = await bathPairInstance.getOutstandingPairCount(); - // if (outstandingPairs > 5) { - // await bathPairInstance.bathScrub(); - // } - // // console.log("outstanding pairs: ", await bathPairInstance.getOutstandingPairCount()); - // }); - // } - - it("Funds are correctly returned to bathTokens", async function () { - await WETHInstance.transfer( - bathQuoteInstance.address, - web3.utils.toWei("0.001"), - { from: accounts[1] } - ); - await DAIInstance.transfer( - bathAssetInstance.address, - web3.utils.toWei("0.001"), - { from: accounts[2] } - ); - - // rebal Pair always tailing risk now if possible... - logIndented( - "cost of rebalance: ", - await bathPairInstance.rebalancePair.estimateGas( - await WETHInstance.balanceOf(bathQuoteInstance.address), - await DAIInstance.balanceOf(bathAssetInstance.address), + }); + it("Zero order can be placed - bid or ask", async function () { + await bathPairInstance.placeMarketMakingTrades( + [WETHInstance.address, DAIInstance.address], + 0, + 0, + bidNumerator, + bidDenominator + ); + await bathPairInstance.placeMarketMakingTrades( + [WETHInstance.address, DAIInstance.address], + askNumerator, + askDenominator, + 0, + 0 + ); + }); + it("New strategist can be added to pools ", async function () { + await bathHouseInstance.approveStrategist(accounts[6]); + await bathPairInstance.placeMarketMakingTrades( + [WETHInstance.address, DAIInstance.address], + askNumerator, + askDenominator, + bidNumerator, + bidDenominator, + { from: accounts[6] } + ); + // await bathPairInstance.removeLiquidity(10, {from: accounts[6]}); + }); + it("Strategist can claim funds", async function () { + await bathPairInstance.strategistBootyClaim( WETHInstance.address, DAIInstance.address - // stratUtilInstance.address, - // "0x0000000000000000000000000000000000000000", - // 0, - // 0 - ) - ); - await bathPairInstance.rebalancePair( - await WETHInstance.balanceOf(bathQuoteInstance.address), - await DAIInstance.balanceOf(bathAssetInstance.address), - WETHInstance.address, - DAIInstance.address - // stratUtilInstance.address, - // "0x0000000000000000000000000000000000000000", - // 0, - // 0 - ); - - assert.equal( - (await WETHInstance.balanceOf(bathQuoteInstance.address)).toString(), - "0" - ); - assert.equal( - (await DAIInstance.balanceOf(bathAssetInstance.address)).toString(), - "0" - ); - }); - it("Strategist can claim funds", async function () { - await bathPairInstance.strategistBootyClaim( - WETHInstance.address, - DAIInstance.address - ); - // TODO: validate this is correct - assert.equal( - (await WETHInstance.balanceOf(accounts[0])).toString(), - "20000000000000" - ); - }); - it("Edge Case: Strategist can take out their own orders to make a new midpoint", async function () { - // const askNumerator = web3.utils.toWei((0.01).toString()); - // const askDenominator = web3.utils.toWei((0.5).toString()); - // const bidNumerator = web3.utils.toWei((0.4).toString()); - // const bidDenominator = web3.utils.toWei((0.01).toString()); - // const assetInstance = await WAYNE.new( - // accounts[8], - // web3.utils.toWei((10000).toString()), - // "WAYNE", - // "WAYNE" - // ); - - await DAIInstance.faucet({ from: accounts[7] }); - await DAIInstance.approve( - rubiconMarketInstance.address, - web3.utils.toWei((1000).toString()), - { from: accounts[7] } - ); - await WETHInstance.deposit({ - from: accounts[8], - value: web3.utils.toWei((2).toString()), + ); + assert.equal( + (await WETHInstance.balanceOf(accounts[0])).toString(), + "20000000000000" + ); }); - await WETHInstance.approve( - rubiconMarketInstance.address, - web3.utils.toWei((1000).toString()), - { from: accounts[8] } - ); + it("Edge Case: Strategist can take out their own orders to make a new midpoint", async function () { + await DAIInstance.faucet({ from: accounts[7] }); + await DAIInstance.approve( + rubiconMarketInstance.address, + web3.utils.toWei((1000).toString()), + { from: accounts[7] } + ); + await WETHInstance.deposit({ + from: accounts[8], + value: web3.utils.toWei((2).toString()), + }); + await WETHInstance.approve( + rubiconMarketInstance.address, + web3.utils.toWei((1000).toString()), + { from: accounts[8] } + ); - await rubiconMarketInstance.offer( - web3.utils.toWei((1).toString()), - DAIInstance.address, - web3.utils.toWei((1).toString()), - WETHInstance.address, - 0, - { from: accounts[7] } - ); - await rubiconMarketInstance.offer( - web3.utils.toWei((1).toString()), - WETHInstance.address, - web3.utils.toWei((2).toString()), - DAIInstance.address, - 0, - { from: accounts[8] } - ); + await rubiconMarketInstance.offer( + web3.utils.toWei((1).toString()), + DAIInstance.address, + web3.utils.toWei((1).toString()), + WETHInstance.address, + 0, + { from: accounts[7] } + ); + await rubiconMarketInstance.offer( + web3.utils.toWei((1).toString()), + WETHInstance.address, + web3.utils.toWei((2).toString()), + DAIInstance.address, + 0, + { from: accounts[8] } + ); - // midpoint around $2 - $1 - await bathPairInstance.placeMarketMakingTrades( - [WETHInstance.address, DAIInstance.address], - web3.utils.toWei((0.2).toString()), - web3.utils.toWei((0.4).toString()), - web3.utils.toWei((1).toString()), - web3.utils.toWei((1).toString()) - ); + // midpoint around $2 - $1 + await bathPairInstance.placeMarketMakingTrades( + [WETHInstance.address, DAIInstance.address], + web3.utils.toWei((0.2).toString()), + web3.utils.toWei((0.4).toString()), + web3.utils.toWei((1).toString()), + web3.utils.toWei((1).toString()) + ); - await bathPairInstance.placeMarketMakingTrades( - [WETHInstance.address, DAIInstance.address], - web3.utils.toWei((0.1).toString()), - web3.utils.toWei((0.3).toString()), - web3.utils.toWei((1).toString()), - web3.utils.toWei((1).toString()) - ); + await bathPairInstance.placeMarketMakingTrades( + [WETHInstance.address, DAIInstance.address], + web3.utils.toWei((0.1).toString()), + web3.utils.toWei((0.3).toString()), + web3.utils.toWei((1).toString()), + web3.utils.toWei((1).toString()) + ); - await bathPairInstance.placeMarketMakingTrades( - [WETHInstance.address, DAIInstance.address], - web3.utils.toWei((0.1).toString()), - web3.utils.toWei((0.3).toString()), - web3.utils.toWei((1).toString()), - web3.utils.toWei((0.7).toString()) - ); + await bathPairInstance.placeMarketMakingTrades( + [WETHInstance.address, DAIInstance.address], + web3.utils.toWei((0.1).toString()), + web3.utils.toWei((0.3).toString()), + web3.utils.toWei((1).toString()), + web3.utils.toWei((0.7).toString()) + ); - await bathPairInstance.placeMarketMakingTrades( - [WETHInstance.address, DAIInstance.address], - web3.utils.toWei((0.2).toString()), - web3.utils.toWei((0.4).toString()), - web3.utils.toWei((1).toString()), - web3.utils.toWei((1).toString()) - ); + await bathPairInstance.placeMarketMakingTrades( + [WETHInstance.address, DAIInstance.address], + web3.utils.toWei((0.2).toString()), + web3.utils.toWei((0.4).toString()), + web3.utils.toWei((1).toString()), + web3.utils.toWei((1).toString()) + ); - await bathPairInstance.placeMarketMakingTrades( - [WETHInstance.address, DAIInstance.address], - web3.utils.toWei((0.2).toString()), - web3.utils.toWei((0.4).toString()), - web3.utils.toWei((1).toString()), - web3.utils.toWei((1).toString()) - ); + await bathPairInstance.placeMarketMakingTrades( + [WETHInstance.address, DAIInstance.address], + web3.utils.toWei((0.2).toString()), + web3.utils.toWei((0.4).toString()), + web3.utils.toWei((1).toString()), + web3.utils.toWei((1).toString()) + ); - await bathPairInstance.placeMarketMakingTrades( - [WETHInstance.address, DAIInstance.address], - web3.utils.toWei((0.2).toString()), - web3.utils.toWei((0.4).toString()), - web3.utils.toWei((1).toString()), - web3.utils.toWei((1).toString()) - ); + await bathPairInstance.placeMarketMakingTrades( + [WETHInstance.address, DAIInstance.address], + web3.utils.toWei((0.2).toString()), + web3.utils.toWei((0.4).toString()), + web3.utils.toWei((1).toString()), + web3.utils.toWei((1).toString()) + ); - await bathPairInstance.placeMarketMakingTrades( - [WETHInstance.address, DAIInstance.address], - web3.utils.toWei((0.2).toString()), - web3.utils.toWei((0.4).toString()), - web3.utils.toWei((1).toString()), - web3.utils.toWei((1).toString()) - ); + await bathPairInstance.placeMarketMakingTrades( + [WETHInstance.address, DAIInstance.address], + web3.utils.toWei((0.2).toString()), + web3.utils.toWei((0.4).toString()), + web3.utils.toWei((1).toString()), + web3.utils.toWei((1).toString()) + ); - //try $5-$3 - await bathPairInstance.placeMarketMakingTrades( - [WETHInstance.address, DAIInstance.address], - web3.utils.toWei((0.1).toString()), - web3.utils.toWei((0.5).toString()), - web3.utils.toWei((3).toString()), - web3.utils.toWei((1).toString()) - ); + //try $5-$3 + await bathPairInstance.placeMarketMakingTrades( + [WETHInstance.address, DAIInstance.address], + web3.utils.toWei((0.1).toString()), + web3.utils.toWei((0.5).toString()), + web3.utils.toWei((3).toString()), + web3.utils.toWei((1).toString()) + ); - //try $0.5 - 0.2 - await bathPairInstance.placeMarketMakingTrades( - [WETHInstance.address, DAIInstance.address], - web3.utils.toWei((0.1).toString()), - web3.utils.toWei((0.05).toString()), - web3.utils.toWei((0.2).toString()), - web3.utils.toWei((1).toString()) - ); + //try $0.5 - 0.2 + await bathPairInstance.placeMarketMakingTrades( + [WETHInstance.address, DAIInstance.address], + web3.utils.toWei((0.1).toString()), + web3.utils.toWei((0.05).toString()), + web3.utils.toWei((0.2).toString()), + web3.utils.toWei((1).toString()) + ); + }); }); - }); -}); + } +); diff --git a/test/oldTests/2_router_test.js b/test/oldTests/2_router_test.js index 1e484cc..ac26d11 100644 --- a/test/oldTests/2_router_test.js +++ b/test/oldTests/2_router_test.js @@ -186,19 +186,6 @@ contract("Rubicon Router Test", async function (accounts) { { from: accounts[2] } ); - // logIndented("got this expect", web3.utils.fromWei(expect)) - // await router.swap( - // web3.utils.toWei((0.25).toString()), - // web3.utils.toWei((0.02495).toString()), // after fees, simply * 1 - 0.2% - // [(await asset1).address, DAIInstance.address, (await asset2).address], - // 20, - // { from: accounts[2] } - // ); - - // assert.equal( - // (await (await asset2).balanceOf(accounts[2])).toString(), - // web3.utils.toWei((0.02495).toString()) - // ); assert.equal(expect.toString(), web3.utils.toWei((0.02495).toString())); }); }); @@ -248,7 +235,7 @@ contract("Rubicon Router Test", async function (accounts) { describe("Random", async function () { // make a thick bid and ask for Asset_1 / DAI - it("WIP - getBook returns data on the real-time order book", async function () { + it("WIP - getBook returns data we like?", async function () { let result = await router.getBookFromPair( asset1.address, asset2.address, From f25763887f222dd90baaf49124e0971dbb5054c4 Mon Sep 17 00:00:00 2001 From: bghughes Date: Mon, 16 May 2022 16:23:11 -0500 Subject: [PATCH 2/2] clean verbiage --- contracts/rubiconPools/BathPair.sol | 1 + contracts/rubiconPools/BathToken.sol | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/rubiconPools/BathPair.sol b/contracts/rubiconPools/BathPair.sol index 06d77d9..23f12ff 100644 --- a/contracts/rubiconPools/BathPair.sol +++ b/contracts/rubiconPools/BathPair.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 +/// @title Strategist's Entrypoint to Rubicon Pools /// @author Rubicon DeFi Inc. - bghughes.eth /// @notice This contract allows a strategist to use user funds in order to market make for a Rubicon pair /// @notice The BathPair is the admin for the pair's liquidity and has many security checks in place diff --git a/contracts/rubiconPools/BathToken.sol b/contracts/rubiconPools/BathToken.sol index 1244bf1..4812b36 100644 --- a/contracts/rubiconPools/BathToken.sol +++ b/contracts/rubiconPools/BathToken.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: BUSL-1.1 +/// @title Single-Asset Tokenized Liquidity /// @author Rubicon DeFi Inc. - bghughes.eth /// @notice This contract represents a single-asset liquidity pool for Rubicon Pools /// @notice Any user can deposit assets into this pool and earn yield from successful strategist market making with their liquidity -/// @notice This contract looks to both BathPairs and the BathHouse as its admin +/// @notice This contract looks to both BathPair and the BathHouse as its admin pragma solidity =0.7.6;