diff --git a/projects/sdk/src/lib/silo.test.ts b/projects/sdk/src/lib/silo.test.ts index a2eac7322a..014aa1b3a1 100644 --- a/projects/sdk/src/lib/silo.test.ts +++ b/projects/sdk/src/lib/silo.test.ts @@ -33,6 +33,7 @@ beforeAll(async () => { await sdk.silo.deposit(sdk.tokens.BEAN, sdk.tokens.BEAN, amount, 0.1, account); }); + describe("Silo Balance loading", () => { describe("getBalance", function () { it("returns an empty object", async () => { diff --git a/protocol/abi/MockBeanstalk.json b/protocol/abi/MockBeanstalk.json index 725ca195b8..56c0381914 100644 --- a/protocol/abi/MockBeanstalk.json +++ b/protocol/abi/MockBeanstalk.json @@ -8451,6 +8451,130 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "entitlementsMatchBalances", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "exploitBurnBeans", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "exploitBurnStalk0", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "exploitBurnStalk1", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "exploitFertilizer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "exploitMintBeans0", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "exploitMintBeans1", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "exploitMintBeans2", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "exploitMintBeans3", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sopWell", + "type": "address" + } + ], + "name": "exploitSop", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "exploitTokenBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "exploitUserDoubleSendTokenExternal", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "exploitUserInternalTokenBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "exploitUserSendTokenExternal0", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "exploitUserSendTokenExternal1", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "exploitUserSendTokenInternal", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -10236,6 +10360,19 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "unripeToken", + "type": "address" + } + ], + "name": "resetUnderlying", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/protocol/contracts/beanstalk/AppStorage.sol b/protocol/contracts/beanstalk/AppStorage.sol index 3a4701975e..6bec2a797c 100644 --- a/protocol/contracts/beanstalk/AppStorage.sol +++ b/protocol/contracts/beanstalk/AppStorage.sol @@ -566,7 +566,10 @@ contract Storage { * @param evenGerminating Stores germinating data during even seasons. * @param whitelistedStatues Stores a list of Whitelist Statues for all tokens that have been Whitelisted and have not had their Whitelist Status manually removed. * @param sopWell Stores the well that will be used upon a SOP. Unintialized until a SOP occurs, and is kept constant afterwards. + * @param internalTokenBalanceTotal Sum of all users internalTokenBalance. * @param barnRaiseWell Stores the well that the Barn Raise adds liquidity to. + * @param fertilizedPaidIndex The total number of Fertilizer Beans that have been sent out to users. + * @param plenty The amount of plenty token held by the contract. */ struct AppStorage { uint8 deprecated_index; @@ -635,4 +638,8 @@ struct AppStorage { mapping(uint32 => Storage.Sr) unclaimedGerminating; Storage.WhitelistStatus[] whitelistStatuses; address sopWell; + // Cumulative internal Balance of tokens. + mapping(IERC20 => uint256) internalTokenBalanceTotal; + uint256 fertilizedPaidIndex; + uint256 plenty; } diff --git a/protocol/contracts/beanstalk/Invariable.sol b/protocol/contracts/beanstalk/Invariable.sol new file mode 100644 index 0000000000..29b23d2661 --- /dev/null +++ b/protocol/contracts/beanstalk/Invariable.sol @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: MIT + +pragma solidity =0.7.6; +pragma experimental ABIEncoderV2; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol"; +import {SignedSafeMath} from "@openzeppelin/contracts/math/SignedSafeMath.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/SafeCast.sol"; + +import {C} from "contracts/C.sol"; +import {AppStorage} from "contracts/beanstalk/AppStorage.sol"; +import {LibAppStorage} from "contracts/libraries/LibAppStorage.sol"; +import {LibWhitelistedTokens} from "contracts/libraries/Silo/LibWhitelistedTokens.sol"; +import {LibUnripe} from "contracts/libraries/LibUnripe.sol"; +import {LibSilo} from "contracts/libraries/Silo/LibSilo.sol"; + +/** + * @author funderbrker + * @title Invariable + * @notice Implements modifiers that maintain protocol wide invariants. + * @dev Every external writing function should use as many non-redundant invariant modifiers as possible. + * @dev https://www.nascent.xyz/idea/youre-writing-require-statements-wrong + **/ +abstract contract Invariable { + using SafeMath for uint256; + using SignedSafeMath for int256; + using SafeCast for uint256; + + /** + * @notice Ensures all user asset entitlements are coverable by contract balances. + * @dev Should be used on every function that can write. Excepting Diamond functions. + */ + modifier fundsSafu() { + _; + address[] memory tokens = getTokensOfInterest(); + ( + uint256[] memory entitlements, + uint256[] memory balances + ) = getTokenEntitlementsAndBalances(tokens); + for (uint256 i; i < tokens.length; i++) { + require(balances[i] >= entitlements[i], "INV: Insufficient token balance"); + } + } + + /** + * @notice Watched token balances do not change and Stalk does not decrease. + * @dev Applicable to the majority of functions, excepting functions that explicitly move assets. + * @dev Roughly akin to a view only check where only routine modifications are allowed (ie mowing). + */ + modifier noNetFlow() { + uint256 initialStalk = LibAppStorage.diamondStorage().s.stalk; + address[] memory tokens = getTokensOfInterest(); + uint256[] memory initialProtocolTokenBalances = getTokenBalances(tokens); + _; + uint256[] memory finalProtocolTokenBalances = getTokenBalances(tokens); + + require( + LibAppStorage.diamondStorage().s.stalk >= initialStalk, + "INV: noNetFlow Stalk decreased" + ); + for (uint256 i; i < tokens.length; i++) { + require( + initialProtocolTokenBalances[i] == finalProtocolTokenBalances[i], + "INV: noNetFlow Token balance changed" + ); + } + } + + /** + * @notice Watched token balances do not decrease and Stalk does not decrease. + * @dev Favor noNetFlow where applicable. + */ + modifier noOutFlow() { + uint256 initialStalk = LibAppStorage.diamondStorage().s.stalk; + address[] memory tokens = getTokensOfInterest(); + uint256[] memory initialProtocolTokenBalances = getTokenBalances(tokens); + _; + uint256[] memory finalProtocolTokenBalances = getTokenBalances(tokens); + + require( + LibAppStorage.diamondStorage().s.stalk >= initialStalk, + "INV: noOutFlow Stalk decreased" + ); + for (uint256 i; i < tokens.length; i++) { + require( + initialProtocolTokenBalances[i] <= finalProtocolTokenBalances[i], + "INV: noOutFlow Token balance decreased" + ); + } + } + + /** + * @notice All except one watched token balances do not decrease. + * @dev Favor noNetFlow or noOutFlow where applicable. + */ + modifier oneOutFlow(address outboundToken) { + address[] memory tokens = getTokensOfInterest(); + uint256[] memory initialProtocolTokenBalances = getTokenBalances(tokens); + _; + uint256[] memory finalProtocolTokenBalances = getTokenBalances(tokens); + + for (uint256 i; i < tokens.length; i++) { + if (tokens[i] == outboundToken) { + continue; + } + require( + initialProtocolTokenBalances[i] <= finalProtocolTokenBalances[i], + "INV: oneOutFlow multiple token balances decreased" + ); + } + } + + /** + * @notice Does not change the supply of Beans. No minting, no burning. + * @dev Applies to all but a very few functions tht explicitly change supply. + */ + modifier noSupplyChange() { + uint256 initialSupply = C.bean().totalSupply(); + _; + require(C.bean().totalSupply() == initialSupply, "INV: Supply changed"); + } + + /** + * @notice Supply of Beans does not increase. No minting. + * @dev Prefer noSupplyChange where applicable. + */ + modifier noSupplyIncrease() { + uint256 initialSupply = C.bean().totalSupply(); + _; + require(C.bean().totalSupply() <= initialSupply, "INV: Supply increased"); + } + + /** + * @notice Which tokens to monitor in the invariants. + */ + function getTokensOfInterest() internal view returns (address[] memory tokens) { + address[] memory whitelistedTokens = LibWhitelistedTokens.getWhitelistedTokens(); + address sopToken = address(LibSilo.getSopToken()); + if (sopToken == address(0)) { + tokens = new address[](whitelistedTokens.length); + } else { + tokens = new address[](whitelistedTokens.length + 1); + tokens[tokens.length - 1] = sopToken; + } + for (uint256 i; i < whitelistedTokens.length; i++) { + tokens[i] = whitelistedTokens[i]; + } + } + + /** + * @notice Get the Beanstalk balance of an ERC20 token. + */ + function getTokenBalances( + address[] memory tokens + ) internal view returns (uint256[] memory balances) { + balances = new uint256[](tokens.length); + for (uint256 i; i < tokens.length; i++) { + balances[i] = IERC20(tokens[i]).balanceOf(address(this)); + } + return balances; + } + + /** + * @notice Get protocol level entitlements and balances for all tokens. + */ + function getTokenEntitlementsAndBalances( + address[] memory tokens + ) internal view returns (uint256[] memory entitlements, uint256[] memory balances) { + AppStorage storage s = LibAppStorage.diamondStorage(); + entitlements = new uint256[](tokens.length); + balances = new uint256[](tokens.length); + for (uint256 i; i < tokens.length; i++) { + entitlements[i] = + s.siloBalances[tokens[i]].deposited + + s.siloBalances[tokens[i]].withdrawn + + s.evenGerminating.deposited[tokens[i]].amount + + s.oddGerminating.deposited[tokens[i]].amount + + s.internalTokenBalanceTotal[IERC20(tokens[i])]; + if (tokens[i] == C.BEAN) { + entitlements[i] += + s.f.harvestable.sub(s.f.harvested) + // unharvestable harvestable beans + s.fertilizedIndex.sub(s.fertilizedPaidIndex) + // unrinsed rinsable beans + s.u[C.UNRIPE_BEAN].balanceOfUnderlying; // unchopped underlying beans + } else if (tokens[i] == LibUnripe._getUnderlyingToken(C.UNRIPE_LP)) { + entitlements[i] += s.u[C.UNRIPE_LP].balanceOfUnderlying; + } + if (s.sopWell != address(0) && tokens[i] == address(LibSilo.getSopToken())) { + entitlements[i] += s.plenty; + } + balances[i] = IERC20(tokens[i]).balanceOf(address(this)); + } + return (entitlements, balances); + } +} diff --git a/protocol/contracts/beanstalk/barn/FertilizerFacet.sol b/protocol/contracts/beanstalk/barn/FertilizerFacet.sol index 178a4f00e9..040a47b092 100644 --- a/protocol/contracts/beanstalk/barn/FertilizerFacet.sol +++ b/protocol/contracts/beanstalk/barn/FertilizerFacet.sol @@ -19,13 +19,14 @@ import {C} from "contracts/C.sol"; import {LibDiamond} from "contracts/libraries/LibDiamond.sol"; import {IWell} from "contracts/interfaces/basin/IWell.sol"; import {LibBarnRaise} from "contracts/libraries/LibBarnRaise.sol"; +import {Invariable} from "contracts/beanstalk/Invariable.sol"; /** * @author Publius * @title FertilizerFacet handles Minting Fertilizer and Rinsing Sprouts earned from Fertilizer. **/ -contract FertilizerFacet { +contract FertilizerFacet is Invariable { using SafeMath for uint256; using SafeCast for uint256; using LibSafeMath128 for uint128; @@ -46,8 +47,12 @@ contract FertilizerFacet { * @param ids The ids of the Fertilizer to rinse. * @param mode The balance to transfer Beans to; see {LibTrasfer.To} */ - function claimFertilized(uint256[] calldata ids, LibTransfer.To mode) external payable { + function claimFertilized( + uint256[] calldata ids, + LibTransfer.To mode + ) external payable fundsSafu noSupplyChange oneOutFlow(C.BEAN) { uint256 amount = C.fertilizer().beanstalkUpdate(LibTractor._user(), ids, s.bpf); + s.fertilizedPaidIndex += amount; LibTransfer.sendToken(C.bean(), amount, LibTractor._user(), mode); } @@ -62,7 +67,7 @@ contract FertilizerFacet { uint256 tokenAmountIn, uint256 minFertilizerOut, uint256 minLPTokensOut - ) external payable returns (uint256 fertilizerAmountOut) { + ) external payable fundsSafu noOutFlow returns (uint256 fertilizerAmountOut) { fertilizerAmountOut = _getMintFertilizerOut( tokenAmountIn, LibBarnRaise.getBarnRaiseToken() @@ -91,8 +96,12 @@ contract FertilizerFacet { /** * @dev Callback from Fertilizer contract in `claimFertilized` function. */ - function payFertilizer(address account, uint256 amount) external payable { + function payFertilizer( + address account, + uint256 amount + ) external payable fundsSafu noSupplyChange oneOutFlow(C.BEAN) { require(msg.sender == C.fertilizerAddress()); + s.fertilizedPaidIndex += amount; LibTransfer.sendToken(C.bean(), amount, account, LibTransfer.To.INTERNAL); } diff --git a/protocol/contracts/beanstalk/barn/UnripeFacet.sol b/protocol/contracts/beanstalk/barn/UnripeFacet.sol index ea44f4cd5d..a6d1977f7c 100644 --- a/protocol/contracts/beanstalk/barn/UnripeFacet.sol +++ b/protocol/contracts/beanstalk/barn/UnripeFacet.sol @@ -19,6 +19,7 @@ import {ReentrancyGuard} from "contracts/beanstalk/ReentrancyGuard.sol"; import {LibLockedUnderlying} from "contracts/libraries/LibLockedUnderlying.sol"; import {LibChop} from "contracts/libraries/LibChop.sol"; import {LibBarnRaise} from "contracts/libraries/LibBarnRaise.sol"; +import {Invariable} from "contracts/beanstalk/Invariable.sol"; import {LibTractor} from "contracts/libraries/LibTractor.sol"; /** @@ -28,7 +29,7 @@ import {LibTractor} from "contracts/libraries/LibTractor.sol"; * managing Unripe Tokens. Also, contains view functions to fetch Unripe Token data. */ -contract UnripeFacet is ReentrancyGuard { +contract UnripeFacet is Invariable, ReentrancyGuard { using SafeERC20 for IERC20; using LibTransfer for IERC20; using SafeMath for uint256; @@ -82,7 +83,7 @@ contract UnripeFacet is ReentrancyGuard { uint256 amount, LibTransfer.From fromMode, LibTransfer.To toMode - ) external payable nonReentrant returns (uint256) { + ) external payable fundsSafu noSupplyChange nonReentrant returns (uint256) { // burn the token from the user address uint256 supply = IBean(unripeToken).totalSupply(); amount = LibTransfer.burnToken(IBean(unripeToken), amount, LibTractor._user(), fromMode); @@ -113,7 +114,7 @@ contract UnripeFacet is ReentrancyGuard { uint256 amount, bytes32[] memory proof, LibTransfer.To mode - ) external payable nonReentrant { + ) external payable fundsSafu noSupplyChange oneOutFlow(token) nonReentrant { bytes32 root = s.u[token].merkleRoot; require(root != bytes32(0), "UnripeClaim: invalid token"); require(!picked(LibTractor._user(), token), "UnripeClaim: already picked"); @@ -288,7 +289,7 @@ contract UnripeFacet is ReentrancyGuard { address unripeToken, address underlyingToken, bytes32 root - ) external payable nonReentrant { + ) external payable fundsSafu noNetFlow noSupplyChange nonReentrant { LibDiamond.enforceIsOwnerOrContract(); s.u[unripeToken].underlyingToken = underlyingToken; s.u[unripeToken].merkleRoot = root; @@ -303,7 +304,7 @@ contract UnripeFacet is ReentrancyGuard { function getUnderlyingToken( address unripeToken ) external view returns (address underlyingToken) { - return s.u[unripeToken].underlyingToken; + return LibUnripe._getUnderlyingToken(unripeToken); } /////////////// UNDERLYING TOKEN MIGRATION ////////////////// @@ -318,7 +319,7 @@ contract UnripeFacet is ReentrancyGuard { function addMigratedUnderlying( address unripeToken, uint256 amount - ) external payable nonReentrant { + ) external payable fundsSafu noNetFlow noSupplyChange nonReentrant { LibDiamond.enforceIsContractOwner(); IERC20(s.u[unripeToken].underlyingToken).safeTransferFrom( LibTractor._user(), @@ -337,7 +338,7 @@ contract UnripeFacet is ReentrancyGuard { function switchUnderlyingToken( address unripeToken, address newUnderlyingToken - ) external payable { + ) external payable fundsSafu noNetFlow noSupplyChange { LibDiamond.enforceIsContractOwner(); require(s.u[unripeToken].balanceOfUnderlying == 0, "Unripe: Underlying balance > 0"); LibUnripe.switchUnderlyingToken(unripeToken, newUnderlyingToken); diff --git a/protocol/contracts/beanstalk/diamond/DiamondCutFacet.sol b/protocol/contracts/beanstalk/diamond/DiamondCutFacet.sol index 587f0a4199..3f7e8cae61 100644 --- a/protocol/contracts/beanstalk/diamond/DiamondCutFacet.sol +++ b/protocol/contracts/beanstalk/diamond/DiamondCutFacet.sol @@ -11,8 +11,9 @@ pragma solidity =0.7.6; import {IDiamondCut} from "contracts/interfaces/IDiamondCut.sol"; import {LibDiamond} from "contracts/libraries/LibDiamond.sol"; +import {Invariable} from "contracts/beanstalk/Invariable.sol"; -contract DiamondCutFacet is IDiamondCut { +contract DiamondCutFacet is Invariable, IDiamondCut { /// @notice Add/replace/remove any number of functions and optionally execute /// a function with delegatecall /// @param _diamondCut Contains the facet addresses and function selectors diff --git a/protocol/contracts/beanstalk/farm/DepotFacet.sol b/protocol/contracts/beanstalk/farm/DepotFacet.sol index ca9c360cad..a52f1cf568 100644 --- a/protocol/contracts/beanstalk/farm/DepotFacet.sol +++ b/protocol/contracts/beanstalk/farm/DepotFacet.sol @@ -8,6 +8,7 @@ pragma experimental ABIEncoderV2; import "contracts/interfaces/IPipeline.sol"; import "contracts/libraries/LibFunction.sol"; import "contracts/libraries/Token/LibEth.sol"; +import {Invariable} from "contracts/beanstalk/Invariable.sol"; /** * @title Depot Facet @@ -16,7 +17,7 @@ import "contracts/libraries/Token/LibEth.sol"; * in the same transaction that loads Ether, Pipes calls to other protocols and unloads Pipeline. **/ -contract DepotFacet { +contract DepotFacet is Invariable { // Pipeline V1.0.1 address private constant PIPELINE = 0xb1bE0000C6B3C62749b5F0c92480146452D15423; @@ -25,7 +26,9 @@ contract DepotFacet { * @param p PipeCall to pipe through Pipeline * @return result PipeCall return value **/ - function pipe(PipeCall calldata p) external payable returns (bytes memory result) { + function pipe( + PipeCall calldata p + ) external payable fundsSafu noSupplyIncrease returns (bytes memory result) { result = IPipeline(PIPELINE).pipe(p); } @@ -37,7 +40,7 @@ contract DepotFacet { **/ function multiPipe( PipeCall[] calldata pipes - ) external payable returns (bytes[] memory results) { + ) external payable fundsSafu noSupplyIncrease returns (bytes[] memory results) { results = IPipeline(PIPELINE).multiPipe(pipes); } @@ -49,7 +52,7 @@ contract DepotFacet { function advancedPipe( AdvancedPipeCall[] calldata pipes, uint256 value - ) external payable returns (bytes[] memory results) { + ) external payable fundsSafu noSupplyIncrease returns (bytes[] memory results) { results = IPipeline(PIPELINE).advancedPipe{value: value}(pipes); LibEth.refundEth(); } @@ -63,7 +66,7 @@ contract DepotFacet { function etherPipe( PipeCall calldata p, uint256 value - ) external payable returns (bytes memory result) { + ) external payable fundsSafu noSupplyIncrease returns (bytes memory result) { result = IPipeline(PIPELINE).pipe{value: value}(p); LibEth.refundEth(); } diff --git a/protocol/contracts/beanstalk/farm/FarmFacet.sol b/protocol/contracts/beanstalk/farm/FarmFacet.sol index 63f412fef3..544c563728 100644 --- a/protocol/contracts/beanstalk/farm/FarmFacet.sol +++ b/protocol/contracts/beanstalk/farm/FarmFacet.sol @@ -10,6 +10,7 @@ import {LibDiamond} from "../../libraries/LibDiamond.sol"; import {LibEth} from "../../libraries/Token/LibEth.sol"; import {AdvancedFarmCall, LibFarm} from "../../libraries/LibFarm.sol"; import {LibFunction} from "../../libraries/LibFunction.sol"; +import {Invariable} from "contracts/beanstalk/Invariable.sol"; /** * @title Farm Facet @@ -18,7 +19,7 @@ import {LibFunction} from "../../libraries/LibFunction.sol"; * Any function stored in Beanstalk's EIP-2535 DiamondStorage can be called as a Farm call. (https://eips.ethereum.org/EIPS/eip-2535) **/ -contract FarmFacet { +contract FarmFacet is Invariable { AppStorage internal s; /** @@ -26,7 +27,9 @@ contract FarmFacet { * @param data The encoded function data for each of the calls * @return results The return data from each of the calls **/ - function farm(bytes[] calldata data) external payable withEth returns (bytes[] memory results) { + function farm( + bytes[] calldata data + ) external payable fundsSafu withEth returns (bytes[] memory results) { results = new bytes[](data.length); for (uint256 i; i < data.length; ++i) { results[i] = LibFarm._farm(data[i]); @@ -41,7 +44,7 @@ contract FarmFacet { **/ function advancedFarm( AdvancedFarmCall[] calldata data - ) external payable withEth returns (bytes[] memory results) { + ) external payable fundsSafu withEth returns (bytes[] memory results) { results = new bytes[](data.length); for (uint256 i = 0; i < data.length; ++i) { results[i] = LibFarm._advancedFarm(data[i], results); diff --git a/protocol/contracts/beanstalk/farm/TokenFacet.sol b/protocol/contracts/beanstalk/farm/TokenFacet.sol index e0c7a61c33..b116ffdb0e 100644 --- a/protocol/contracts/beanstalk/farm/TokenFacet.sol +++ b/protocol/contracts/beanstalk/farm/TokenFacet.sol @@ -14,12 +14,13 @@ import "contracts/libraries/Token/LibTokenPermit.sol"; import "contracts/libraries/Token/LibTokenApprove.sol"; import "../AppStorage.sol"; import "../ReentrancyGuard.sol"; +import {Invariable} from "contracts/beanstalk/Invariable.sol"; /** * @author Publius * @title TokenFacet handles transfers of assets */ -contract TokenFacet is IERC1155Receiver, ReentrancyGuard { +contract TokenFacet is Invariable, IERC1155Receiver, ReentrancyGuard { struct Balance { uint256 internalBalance; uint256 externalBalance; @@ -56,12 +57,12 @@ contract TokenFacet is IERC1155Receiver, ReentrancyGuard { uint256 amount, LibTransfer.From fromMode, LibTransfer.To toMode - ) external payable { + ) external payable fundsSafu noSupplyChange oneOutFlow(address(token)) { LibTransfer.transferToken(token, LibTractor._user(), recipient, amount, fromMode, toMode); } /** - * @notice transfers a token from `sender` to an `recipient` Internal balance. + * @notice transfers a token from `sender` to an `recipient` from Internal balance. * @dev differs from transferToken as sender != user. */ function transferInternalTokenFrom( @@ -70,7 +71,7 @@ contract TokenFacet is IERC1155Receiver, ReentrancyGuard { address recipient, uint256 amount, LibTransfer.To toMode - ) external payable nonReentrant { + ) external payable fundsSafu noSupplyChange oneOutFlow(address(token)) nonReentrant { LibTransfer.transferToken( token, sender, @@ -95,7 +96,7 @@ contract TokenFacet is IERC1155Receiver, ReentrancyGuard { address spender, IERC20 token, uint256 amount - ) external payable nonReentrant { + ) external payable fundsSafu noNetFlow noSupplyChange nonReentrant { LibTokenApprove.approve(LibTractor._user(), spender, token, amount); } @@ -106,7 +107,7 @@ contract TokenFacet is IERC1155Receiver, ReentrancyGuard { address spender, IERC20 token, uint256 addedValue - ) public virtual nonReentrant returns (bool) { + ) public virtual fundsSafu noNetFlow noSupplyChange nonReentrant returns (bool) { LibTokenApprove.approve( LibTractor._user(), spender, @@ -123,7 +124,7 @@ contract TokenFacet is IERC1155Receiver, ReentrancyGuard { address spender, IERC20 token, uint256 subtractedValue - ) public virtual nonReentrant returns (bool) { + ) public virtual fundsSafu noNetFlow noSupplyChange nonReentrant returns (bool) { uint256 currentAllowance = LibTokenApprove.allowance(LibTractor._user(), spender, token); require(currentAllowance >= subtractedValue, "Silo: decreased allowance below zero"); LibTokenApprove.approve( @@ -160,7 +161,7 @@ contract TokenFacet is IERC1155Receiver, ReentrancyGuard { uint8 v, bytes32 r, bytes32 s - ) external payable nonReentrant { + ) external payable fundsSafu noNetFlow noSupplyChange nonReentrant { LibTokenPermit.permit(owner, spender, token, value, deadline, v, r, s); LibTokenApprove.approve(owner, spender, IERC20(token), value); } @@ -219,7 +220,10 @@ contract TokenFacet is IERC1155Receiver, ReentrancyGuard { /** * @notice wraps ETH into WETH. */ - function wrapEth(uint256 amount, LibTransfer.To mode) external payable { + function wrapEth( + uint256 amount, + LibTransfer.To mode + ) external payable fundsSafu noNetFlow noSupplyChange { LibWeth.wrap(amount, mode); LibEth.refundEth(); } @@ -227,7 +231,10 @@ contract TokenFacet is IERC1155Receiver, ReentrancyGuard { /** * @notice unwraps WETH into ETH. */ - function unwrapEth(uint256 amount, LibTransfer.From mode) external payable { + function unwrapEth( + uint256 amount, + LibTransfer.From mode + ) external payable fundsSafu noNetFlow noSupplyChange { LibWeth.unwrap(amount, mode); } diff --git a/protocol/contracts/beanstalk/farm/TokenSupportFacet.sol b/protocol/contracts/beanstalk/farm/TokenSupportFacet.sol index fc0946d41f..fd26ebadd8 100644 --- a/protocol/contracts/beanstalk/farm/TokenSupportFacet.sol +++ b/protocol/contracts/beanstalk/farm/TokenSupportFacet.sol @@ -9,7 +9,8 @@ import "@openzeppelin/contracts/drafts/IERC20Permit.sol"; import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "../../interfaces/IERC4494.sol"; -import "../../libraries/LibTractor.sol"; +import {Invariable} from "contracts/beanstalk/Invariable.sol"; +import {LibTractor} from "../../libraries/LibTractor.sol"; /** * @author Publius @@ -18,7 +19,7 @@ import "../../libraries/LibTractor.sol"; * @dev To transfer ERC-20 tokens, use {TokenFacet.transferToken}. **/ -contract TokenSupportFacet { +contract TokenSupportFacet is Invariable { /** * * ERC-20 @@ -36,7 +37,7 @@ contract TokenSupportFacet { uint8 v, bytes32 r, bytes32 s - ) public payable { + ) public payable fundsSafu noNetFlow noSupplyChange { token.permit(owner, spender, value, deadline, v, r, s); } @@ -50,7 +51,11 @@ contract TokenSupportFacet { * @notice Execute an ERC-721 token transfer * @dev Wraps {IERC721-safeBatchTransferFrom}. **/ - function transferERC721(IERC721 token, address to, uint256 id) external payable { + function transferERC721( + IERC721 token, + address to, + uint256 id + ) external payable fundsSafu noNetFlow noSupplyChange { token.safeTransferFrom(LibTractor._user(), to, id); } @@ -64,7 +69,7 @@ contract TokenSupportFacet { uint256 tokenId, uint256 deadline, bytes memory sig - ) external payable { + ) external payable fundsSafu noNetFlow noSupplyChange { token.permit(spender, tokenId, deadline, sig); } @@ -83,7 +88,7 @@ contract TokenSupportFacet { address to, uint256 id, uint256 value - ) external payable { + ) external payable fundsSafu noNetFlow noSupplyChange { token.safeTransferFrom(LibTractor._user(), to, id, value, new bytes(0)); } @@ -96,7 +101,7 @@ contract TokenSupportFacet { address to, uint256[] calldata ids, uint256[] calldata values - ) external payable { + ) external payable fundsSafu noNetFlow noSupplyChange { token.safeBatchTransferFrom(LibTractor._user(), to, ids, values, new bytes(0)); } } diff --git a/protocol/contracts/beanstalk/field/FieldFacet.sol b/protocol/contracts/beanstalk/field/FieldFacet.sol index be891594dc..37e2059ca3 100644 --- a/protocol/contracts/beanstalk/field/FieldFacet.sol +++ b/protocol/contracts/beanstalk/field/FieldFacet.sol @@ -15,13 +15,14 @@ import {LibPRBMath} from "contracts/libraries/LibPRBMath.sol"; import {LibSafeMath32} from "contracts/libraries/LibSafeMath32.sol"; import {LibSafeMath128} from "contracts/libraries/LibSafeMath128.sol"; import {ReentrancyGuard} from "../ReentrancyGuard.sol"; +import {Invariable} from "contracts/beanstalk/Invariable.sol"; /** * @title FieldFacet * @author Publius, Brean * @notice The Field is where Beans are Sown and Pods are Harvested. */ -contract FieldFacet is ReentrancyGuard { +contract FieldFacet is Invariable, ReentrancyGuard { using SafeMath for uint256; using LibPRBMath for uint256; using LibSafeMath32 for uint32; @@ -75,7 +76,7 @@ contract FieldFacet is ReentrancyGuard { uint256 beans, uint256 minTemperature, LibTransfer.From mode - ) external payable returns (uint256 pods) { + ) external payable fundsSafu noSupplyIncrease oneOutFlow(C.BEAN) returns (uint256 pods) { pods = sowWithMin(beans, minTemperature, beans, mode); } @@ -93,7 +94,7 @@ contract FieldFacet is ReentrancyGuard { uint256 minTemperature, uint256 minSoil, LibTransfer.From mode - ) public payable returns (uint256 pods) { + ) public payable fundsSafu noSupplyIncrease oneOutFlow(C.BEAN) returns (uint256 pods) { // `soil` is the remaining Soil (uint256 soil, uint256 _morningTemperature, bool abovePeg) = _totalSoilAndTemperature(); @@ -139,7 +140,10 @@ contract FieldFacet is ReentrancyGuard { * Pods are "burned" when the corresponding Plot is deleted from * `s.a[account].field.plots`. */ - function harvest(uint256[] calldata plots, LibTransfer.To mode) external payable { + function harvest( + uint256[] calldata plots, + LibTransfer.To mode + ) external payable fundsSafu noSupplyChange oneOutFlow(C.BEAN) { uint256 beansHarvested = _harvest(plots); LibTransfer.sendToken(C.bean(), beansHarvested, LibTractor._user(), mode); } diff --git a/protocol/contracts/beanstalk/init/InitInvariants.sol b/protocol/contracts/beanstalk/init/InitInvariants.sol new file mode 100644 index 0000000000..0fbf4d34cc --- /dev/null +++ b/protocol/contracts/beanstalk/init/InitInvariants.sol @@ -0,0 +1,41 @@ +/* + SPDX-License-Identifier: MIT +*/ + +pragma solidity =0.7.6; +pragma experimental ABIEncoderV2; + +import {AppStorage} from "contracts/beanstalk/AppStorage.sol"; +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import {C} from "contracts/C.sol"; +import {LibDiamond} from "contracts/libraries/LibDiamond.sol"; + +// NOTE: Values are arbitrary placeholders. Need to be populated with correct values at a snapshot point. + +/** + * @author funderbrker + * @notice Initializes the new storage variables underlying invariants. + */ +contract InitInvariants { + AppStorage internal s; + + function init() external { + setInternalTokenBalances(); + + // TODO: Get exact from future snapshot. + s.fertilizedPaidIndex = 3_500_000_000_000; + + // TODO: Get exact amount. May be 0. + // TODO: Ensure SopWell/SopToken initialization is compatible with the logic between here and there. + s.plenty = 0; + } + + function setInternalTokenBalances() internal { + // TODO: Deconstruct s.internalTokenBalance offchain and set all tokens and all totals here. + s.internalTokenBalanceTotal[IERC20(C.BEAN)] = 115611612399; + s.internalTokenBalanceTotal[IERC20(C.BEAN_ETH_WELL)] = 0; + s.internalTokenBalanceTotal[IERC20(C.CURVE_BEAN_METAPOOL)] = 9238364833184139286; + s.internalTokenBalanceTotal[IERC20(C.UNRIPE_BEAN)] = 9001888; + s.internalTokenBalanceTotal[IERC20(C.UNRIPE_LP)] = 12672419462; + } +} diff --git a/protocol/contracts/beanstalk/market/MarketplaceFacet/MarketplaceFacet.sol b/protocol/contracts/beanstalk/market/MarketplaceFacet/MarketplaceFacet.sol index 46f6f82734..38c831736c 100644 --- a/protocol/contracts/beanstalk/market/MarketplaceFacet/MarketplaceFacet.sol +++ b/protocol/contracts/beanstalk/market/MarketplaceFacet/MarketplaceFacet.sol @@ -6,14 +6,15 @@ pragma solidity =0.7.6; pragma experimental ABIEncoderV2; import "./Order.sol"; -import "contracts/libraries/LibTractor.sol"; +import {Invariable} from "contracts/beanstalk/Invariable.sol"; +import {LibTractor} from "contracts/libraries/LibTractor.sol"; /** * @author Beanjoyer, Malteasy * @title Pod Marketplace v2 **/ -contract MarketplaceFacet is Order { +contract MarketplaceFacet is Invariable, Order { /* * Pod Listing */ @@ -29,7 +30,7 @@ contract MarketplaceFacet is Order { uint256 maxHarvestableIndex, uint256 minFillAmount, LibTransfer.To mode - ) external payable { + ) external payable fundsSafu noNetFlow noSupplyChange { _createPodListing( index, start, @@ -49,7 +50,7 @@ contract MarketplaceFacet is Order { uint256 minFillAmount, bytes calldata pricingFunction, LibTransfer.To mode - ) external payable { + ) external payable fundsSafu noNetFlow noSupplyChange { _createPodListingV2( index, start, @@ -66,7 +67,7 @@ contract MarketplaceFacet is Order { PodListing calldata l, uint256 beanAmount, LibTransfer.From mode - ) external payable { + ) external payable fundsSafu noSupplyChange oneOutFlow(C.BEAN) { beanAmount = LibTransfer.transferToken( C.bean(), LibTractor._user(), @@ -83,7 +84,7 @@ contract MarketplaceFacet is Order { uint256 beanAmount, bytes calldata pricingFunction, LibTransfer.From mode - ) external payable { + ) external payable fundsSafu noSupplyChange oneOutFlow(C.BEAN) { beanAmount = LibTransfer.transferToken( C.bean(), LibTractor._user(), @@ -96,7 +97,7 @@ contract MarketplaceFacet is Order { } // Cancel - function cancelPodListing(uint256 index) external payable { + function cancelPodListing(uint256 index) external payable fundsSafu noNetFlow noSupplyChange { _cancelPodListing(LibTractor._user(), index); } @@ -116,7 +117,7 @@ contract MarketplaceFacet is Order { uint256 maxPlaceInLine, uint256 minFillAmount, LibTransfer.From mode - ) external payable returns (bytes32 id) { + ) external payable fundsSafu noSupplyChange noOutFlow returns (bytes32 id) { beanAmount = LibTransfer.receiveToken(C.bean(), beanAmount, LibTractor._user(), mode); return _createPodOrder(beanAmount, pricePerPod, maxPlaceInLine, minFillAmount); } @@ -127,7 +128,7 @@ contract MarketplaceFacet is Order { uint256 minFillAmount, bytes calldata pricingFunction, LibTransfer.From mode - ) external payable returns (bytes32 id) { + ) external payable fundsSafu noSupplyChange noOutFlow returns (bytes32 id) { beanAmount = LibTransfer.receiveToken(C.bean(), beanAmount, LibTractor._user(), mode); return _createPodOrderV2(beanAmount, maxPlaceInLine, minFillAmount, pricingFunction); } @@ -139,7 +140,7 @@ contract MarketplaceFacet is Order { uint256 start, uint256 amount, LibTransfer.To mode - ) external payable { + ) external payable fundsSafu noSupplyChange oneOutFlow(C.BEAN) { _fillPodOrder(o, index, start, amount, mode); } @@ -150,7 +151,7 @@ contract MarketplaceFacet is Order { uint256 amount, bytes calldata pricingFunction, LibTransfer.To mode - ) external payable { + ) external payable fundsSafu noSupplyChange oneOutFlow(C.BEAN) { _fillPodOrderV2(o, index, start, amount, pricingFunction, mode); } @@ -160,7 +161,7 @@ contract MarketplaceFacet is Order { uint256 maxPlaceInLine, uint256 minFillAmount, LibTransfer.To mode - ) external payable { + ) external payable fundsSafu noSupplyChange oneOutFlow(C.BEAN) { _cancelPodOrder(pricePerPod, maxPlaceInLine, minFillAmount, mode); } @@ -169,7 +170,7 @@ contract MarketplaceFacet is Order { uint256 minFillAmount, bytes calldata pricingFunction, LibTransfer.To mode - ) external payable { + ) external payable fundsSafu noSupplyChange oneOutFlow(C.BEAN) { _cancelPodOrderV2(maxPlaceInLine, minFillAmount, pricingFunction, mode); } @@ -210,7 +211,7 @@ contract MarketplaceFacet is Order { uint256 id, uint256 start, uint256 end - ) external payable nonReentrant { + ) external payable fundsSafu noNetFlow noSupplyChange nonReentrant { require( sender != address(0) && recipient != address(0), "Field: Transfer to/from 0 address." @@ -231,7 +232,10 @@ contract MarketplaceFacet is Order { _transferPlot(sender, recipient, id, start, amount); } - function approvePods(address spender, uint256 amount) external payable nonReentrant { + function approvePods( + address spender, + uint256 amount + ) external payable fundsSafu noNetFlow noSupplyChange nonReentrant { require(spender != address(0), "Field: Pod Approve to 0 address."); setAllowancePods(LibTractor._user(), spender, amount); emit PodApproval(LibTractor._user(), spender, amount); diff --git a/protocol/contracts/beanstalk/silo/ApprovalFacet.sol b/protocol/contracts/beanstalk/silo/ApprovalFacet.sol index 3c34dcd453..7010105170 100644 --- a/protocol/contracts/beanstalk/silo/ApprovalFacet.sol +++ b/protocol/contracts/beanstalk/silo/ApprovalFacet.sol @@ -15,13 +15,14 @@ import "./SiloFacet/TokenSilo.sol"; import "contracts/libraries/LibSafeMath32.sol"; import "contracts/libraries/Convert/LibConvert.sol"; import "../ReentrancyGuard.sol"; -import "contracts/libraries/LibTractor.sol"; +import {Invariable} from "contracts/beanstalk/Invariable.sol"; +import {LibTractor} from "contracts/libraries/LibTractor.sol"; /** * @author publius, pizzaman1337 * @title Handles Approval related functions for the Silo **/ -contract ApprovalFacet is ReentrancyGuard { +contract ApprovalFacet is Invariable, ReentrancyGuard { using SafeMath for uint256; event DepositApproval( @@ -47,7 +48,7 @@ contract ApprovalFacet is ReentrancyGuard { address spender, address token, uint256 amount - ) external payable nonReentrant { + ) external payable fundsSafu noNetFlow noSupplyChange nonReentrant { require(spender != address(0), "approve from the zero address"); require(token != address(0), "approve to the zero address"); LibSiloPermit._approveDeposit(LibTractor._user(), spender, token, amount); @@ -64,7 +65,7 @@ contract ApprovalFacet is ReentrancyGuard { address spender, address token, uint256 addedValue - ) public virtual nonReentrant returns (bool) { + ) public virtual fundsSafu noNetFlow noSupplyChange nonReentrant returns (bool) { LibSiloPermit._approveDeposit( LibTractor._user(), spender, @@ -85,7 +86,7 @@ contract ApprovalFacet is ReentrancyGuard { address spender, address token, uint256 subtractedValue - ) public virtual nonReentrant returns (bool) { + ) public virtual fundsSafu noNetFlow noSupplyChange nonReentrant returns (bool) { uint256 currentAllowance = depositAllowance(LibTractor._user(), spender, token); require(currentAllowance >= subtractedValue, "Silo: decreased allowance below zero"); LibSiloPermit._approveDeposit( @@ -126,7 +127,7 @@ contract ApprovalFacet is ReentrancyGuard { uint8 v, bytes32 r, bytes32 s - ) external payable nonReentrant { + ) external payable fundsSafu noNetFlow noSupplyChange nonReentrant { LibSiloPermit.permits(owner, spender, tokens, values, deadline, v, r, s); for (uint256 i; i < tokens.length; ++i) { LibSiloPermit._approveDeposit(owner, spender, tokens[i], values[i]); @@ -154,7 +155,7 @@ contract ApprovalFacet is ReentrancyGuard { uint8 v, bytes32 r, bytes32 s - ) external payable nonReentrant { + ) external payable fundsSafu noNetFlow noSupplyChange nonReentrant { LibSiloPermit.permit(owner, spender, token, value, deadline, v, r, s); LibSiloPermit._approveDeposit(owner, spender, token, value); } @@ -188,7 +189,10 @@ contract ApprovalFacet is ReentrancyGuard { } // ERC1155 Approvals - function setApprovalForAll(address spender, bool approved) external { + function setApprovalForAll( + address spender, + bool approved + ) external fundsSafu noNetFlow noSupplyChange { s.a[LibTractor._user()].isApprovedForAll[spender] = approved; emit ApprovalForAll(LibTractor._user(), spender, approved); } diff --git a/protocol/contracts/beanstalk/silo/ConvertFacet.sol b/protocol/contracts/beanstalk/silo/ConvertFacet.sol index 8c059f9aae..da2241423b 100644 --- a/protocol/contracts/beanstalk/silo/ConvertFacet.sol +++ b/protocol/contracts/beanstalk/silo/ConvertFacet.sol @@ -17,12 +17,13 @@ import {SafeCast} from "@openzeppelin/contracts/utils/SafeCast.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {LibConvert} from "contracts/libraries/Convert/LibConvert.sol"; import {LibGerminate} from "contracts/libraries/Silo/LibGerminate.sol"; +import {Invariable} from "contracts/beanstalk/Invariable.sol"; /** * @author Publius, Brean, DeadManWalking * @title ConvertFacet handles converting Deposited assets within the Silo. **/ -contract ConvertFacet is ReentrancyGuard { +contract ConvertFacet is Invariable, ReentrancyGuard { using SafeMath for uint256; using SafeCast for uint256; using LibSafeMath32 for uint32; @@ -73,6 +74,9 @@ contract ConvertFacet is ReentrancyGuard { ) external payable + fundsSafu + noSupplyChange + // TODO: add oneOutFlow(tokenIn) when pipelineConvert merges. nonReentrant returns (int96 toStem, uint256 fromAmount, uint256 toAmount, uint256 fromBdv, uint256 toBdv) { diff --git a/protocol/contracts/beanstalk/silo/EnrootFacet.sol b/protocol/contracts/beanstalk/silo/EnrootFacet.sol index b1e6a58b51..c2be39f3b5 100644 --- a/protocol/contracts/beanstalk/silo/EnrootFacet.sol +++ b/protocol/contracts/beanstalk/silo/EnrootFacet.sol @@ -11,12 +11,13 @@ import "contracts/libraries/Silo/LibTokenSilo.sol"; import "./SiloFacet/Silo.sol"; import "contracts/libraries/LibSafeMath32.sol"; import "../ReentrancyGuard.sol"; +import {Invariable} from "contracts/beanstalk/Invariable.sol"; /** * @author Publius * @title Enroot Facet handles enrooting Update Deposits **/ -contract EnrootFacet is ReentrancyGuard { +contract EnrootFacet is Invariable, ReentrancyGuard { using SafeMath for uint256; using SafeCast for uint256; @@ -77,34 +78,37 @@ contract EnrootFacet is ReentrancyGuard { address token, int96 stem, uint256 amount - ) external payable nonReentrant mowSender(token) { + ) external payable fundsSafu noNetFlow noSupplyChange nonReentrant mowSender(token) { require(s.u[token].underlyingToken != address(0), "Silo: token not unripe"); - // remove Deposit and Redeposit with new BDV - uint256 ogBDV = LibTokenSilo.removeDepositFromAccount( - LibTractor._user(), - token, - stem, - amount - ); + uint256 deltaBDV; + { + // remove Deposit and Redeposit with new BDV + uint256 ogBDV = LibTokenSilo.removeDepositFromAccount( + LibTractor._user(), + token, + stem, + amount + ); - // Remove Deposit does not emit an event, while Add Deposit does. - emit RemoveDeposit(LibTractor._user(), token, stem, amount, ogBDV); + // Remove Deposit does not emit an event, while Add Deposit does. + emit RemoveDeposit(LibTractor._user(), token, stem, amount, ogBDV); - // Calculate the current BDV for `amount` of `token` and add a Deposit. - uint256 newBDV = LibTokenSilo.beanDenominatedValue(token, amount); + // Calculate the current BDV for `amount` of `token` and add a Deposit. + uint256 newBDV = LibTokenSilo.beanDenominatedValue(token, amount); - LibTokenSilo.addDepositToAccount( - LibTractor._user(), - token, - stem, - amount, - newBDV, - LibTokenSilo.Transfer.noEmitTransferSingle - ); // emits AddDeposit event + LibTokenSilo.addDepositToAccount( + LibTractor._user(), + token, + stem, + amount, + newBDV, + LibTokenSilo.Transfer.noEmitTransferSingle + ); // emits AddDeposit event - // Calculate the difference in BDV. Reverts if `ogBDV > newBDV`. - uint256 deltaBDV = newBDV.sub(ogBDV); + // Calculate the difference in BDV. Reverts if `ogBDV > newBDV`. + deltaBDV = newBDV.sub(ogBDV); + } LibTokenSilo.incrementTotalDepositedBdv(token, deltaBDV); @@ -134,7 +138,7 @@ contract EnrootFacet is ReentrancyGuard { address token, int96[] calldata stems, uint256[] calldata amounts - ) external payable nonReentrant mowSender(token) { + ) external payable fundsSafu noNetFlow noSupplyChange nonReentrant mowSender(token) { require(s.u[token].underlyingToken != address(0), "Silo: token not unripe"); // First, remove Deposits because every deposit is in a different season, // we need to get the total Stalk, not just BDV. diff --git a/protocol/contracts/beanstalk/silo/MigrationFacet.sol b/protocol/contracts/beanstalk/silo/MigrationFacet.sol index 4684bfa30d..1584a9b8f1 100644 --- a/protocol/contracts/beanstalk/silo/MigrationFacet.sol +++ b/protocol/contracts/beanstalk/silo/MigrationFacet.sol @@ -14,12 +14,13 @@ import "contracts/libraries/Silo/LibTokenSilo.sol"; import "contracts/libraries/Silo/LibLegacyTokenSilo.sol"; import "contracts/libraries/Convert/LibConvert.sol"; import "contracts/libraries/LibSafeMath32.sol"; +import {Invariable} from "contracts/beanstalk/Invariable.sol"; /** * @author pizzaman1337 * @title Handles Migration related functions for the new Silo **/ -contract MigrationFacet is ReentrancyGuard { +contract MigrationFacet is Invariable, ReentrancyGuard { /** * @notice Migrates farmer's deposits from old (seasons based) to new silo (stems based). * @param account Address of the account to migrate @@ -45,7 +46,14 @@ contract MigrationFacet is ReentrancyGuard { uint256 stalkDiff, uint256 seedsDiff, bytes32[] calldata proof - ) external payable { + ) + external + payable + // NOTE: Stack too deep when using noNetFlow invariant. + fundsSafu + noSupplyChange + { + // noNetFlow uint256 seedsVariance = LibLegacyTokenSilo._mowAndMigrate( account, tokens, @@ -70,7 +78,9 @@ contract MigrationFacet is ReentrancyGuard { * but they currently have no deposits, then this function can be used to migrate * their account to the new silo using less gas. */ - function mowAndMigrateNoDeposits(address account) external payable { + function mowAndMigrateNoDeposits( + address account + ) external payable fundsSafu noNetFlow noSupplyChange { LibLegacyTokenSilo._migrateNoDeposits(account); } diff --git a/protocol/contracts/beanstalk/silo/SiloFacet/Silo.sol b/protocol/contracts/beanstalk/silo/SiloFacet/Silo.sol index 21125310cd..f507bbc2d0 100644 --- a/protocol/contracts/beanstalk/silo/SiloFacet/Silo.sol +++ b/protocol/contracts/beanstalk/silo/SiloFacet/Silo.sol @@ -143,11 +143,10 @@ contract Silo is ReentrancyGuard { function _claimPlenty(address account) internal { // Plenty is earned in the form of the non-Bean token in the SOP Well. uint256 plenty = s.a[account].sop.plenty; - IWell well = IWell(s.sopWell); - IERC20[] memory tokens = well.tokens(); - IERC20 sopToken = tokens[0] != C.bean() ? tokens[0] : tokens[1]; + IERC20 sopToken = LibSilo.getSopToken(); sopToken.safeTransfer(account, plenty); delete s.a[account].sop.plenty; + s.plenty -= plenty; emit ClaimPlenty(account, address(sopToken), plenty); } diff --git a/protocol/contracts/beanstalk/silo/SiloFacet/SiloFacet.sol b/protocol/contracts/beanstalk/silo/SiloFacet/SiloFacet.sol index 2c3e34f78a..2d3a7e7a92 100644 --- a/protocol/contracts/beanstalk/silo/SiloFacet/SiloFacet.sol +++ b/protocol/contracts/beanstalk/silo/SiloFacet/SiloFacet.sol @@ -9,6 +9,7 @@ import "./TokenSilo.sol"; import {LibTractor} from "contracts/libraries/LibTractor.sol"; import "contracts/libraries/Token/LibTransfer.sol"; import "contracts/libraries/Silo/LibSiloPermit.sol"; +import {Invariable} from "contracts/beanstalk/Invariable.sol"; /** * @title SiloFacet @@ -24,7 +25,7 @@ import "contracts/libraries/Silo/LibSiloPermit.sol"; * * */ -contract SiloFacet is TokenSilo { +contract SiloFacet is Invariable, TokenSilo { using SafeMath for uint256; using LibSafeMath32 for uint32; @@ -52,6 +53,9 @@ contract SiloFacet is TokenSilo { ) external payable + fundsSafu + noSupplyChange + noOutFlow nonReentrant mowSender(token) returns (uint256 amount, uint256 _bdv, int96 stem) @@ -87,7 +91,7 @@ contract SiloFacet is TokenSilo { int96 stem, uint256 amount, LibTransfer.To mode - ) external payable mowSender(token) nonReentrant { + ) external payable fundsSafu noSupplyChange oneOutFlow(token) mowSender(token) nonReentrant { _withdrawDeposit(LibTractor._user(), token, stem, amount); LibTransfer.sendToken(IERC20(token), amount, LibTractor._user(), mode); } @@ -111,7 +115,7 @@ contract SiloFacet is TokenSilo { int96[] calldata stems, uint256[] calldata amounts, LibTransfer.To mode - ) external payable mowSender(token) nonReentrant { + ) external payable fundsSafu noSupplyChange oneOutFlow(token) mowSender(token) nonReentrant { uint256 amount = _withdrawDeposits(LibTractor._user(), token, stems, amounts); LibTransfer.sendToken(IERC20(token), amount, LibTractor._user(), mode); } @@ -139,7 +143,7 @@ contract SiloFacet is TokenSilo { address token, int96 stem, uint256 amount - ) public payable nonReentrant returns (uint256 _bdv) { + ) public payable fundsSafu noNetFlow noSupplyChange nonReentrant returns (uint256 _bdv) { if (sender != LibTractor._user()) { LibSiloPermit._spendDepositAllowance(sender, LibTractor._user(), token, amount); } @@ -171,7 +175,15 @@ contract SiloFacet is TokenSilo { address token, int96[] calldata stem, uint256[] calldata amounts - ) public payable nonReentrant returns (uint256[] memory bdvs) { + ) + public + payable + fundsSafu + noNetFlow + noSupplyChange + nonReentrant + returns (uint256[] memory bdvs) + { require(amounts.length > 0, "Silo: amounts array is empty"); uint256 totalAmount; for (uint256 i = 0; i < amounts.length; ++i) { @@ -207,7 +219,7 @@ contract SiloFacet is TokenSilo { uint256 depositId, uint256 amount, bytes calldata - ) external { + ) external fundsSafu noNetFlow noSupplyChange { require(recipient != address(0), "ERC1155: transfer to the zero address"); // allowance requirements are checked in transferDeposit (address token, int96 cumulativeGrownStalkPerBDV) = LibBytes.unpackAddressAndStem( @@ -233,7 +245,7 @@ contract SiloFacet is TokenSilo { uint256[] calldata depositIds, uint256[] calldata amounts, bytes calldata - ) external { + ) external fundsSafu noNetFlow noSupplyChange { require( depositIds.length == amounts.length, "Silo: depositIDs and amounts arrays must be the same length" @@ -254,12 +266,18 @@ contract SiloFacet is TokenSilo { * @notice Claim Grown Stalk for `account`. * @dev See {Silo-_mow}. */ - function mow(address account, address token) external payable { + function mow( + address account, + address token + ) external payable fundsSafu noNetFlow noSupplyChange { LibSilo._mow(account, token); } //function to mow multiple tokens given an address - function mowMultiple(address account, address[] calldata tokens) external payable { + function mowMultiple( + address account, + address[] calldata tokens + ) external payable fundsSafu noNetFlow noSupplyChange { for (uint256 i; i < tokens.length; ++i) { LibSilo._mow(account, tokens[i]); } @@ -280,14 +298,27 @@ contract SiloFacet is TokenSilo { * In practice, when Seeds are Planted, all Earned Beans are Deposited in * the current Season. */ - function plant() external payable returns (uint256 beans, int96 stem) { + function plant() + external + payable + fundsSafu + noNetFlow + noSupplyChange + returns (uint256 beans, int96 stem) + { return _plant(LibTractor._user()); } /** * @notice Claim rewards from a Flood (Was Season of Plenty) */ - function claimPlenty() external payable { + function claimPlenty() + external + payable + fundsSafu + noSupplyChange + oneOutFlow(address(LibSilo.getSopToken())) + { _claimPlenty(LibTractor._user()); } } diff --git a/protocol/contracts/beanstalk/silo/WhitelistFacet/WhitelistFacet.sol b/protocol/contracts/beanstalk/silo/WhitelistFacet/WhitelistFacet.sol index b34ba483a6..4a2437c3ac 100644 --- a/protocol/contracts/beanstalk/silo/WhitelistFacet/WhitelistFacet.sol +++ b/protocol/contracts/beanstalk/silo/WhitelistFacet/WhitelistFacet.sol @@ -9,6 +9,7 @@ import {LibDiamond} from "contracts/libraries/LibDiamond.sol"; import {LibWhitelist} from "contracts/libraries/Silo/LibWhitelist.sol"; import {AppStorage} from "contracts/beanstalk/AppStorage.sol"; import {WhitelistedTokens} from "contracts/beanstalk/silo/WhitelistFacet/WhitelistedTokens.sol"; +import {Invariable} from "contracts/beanstalk/Invariable.sol"; /** * @author Publius @@ -16,12 +17,12 @@ import {WhitelistedTokens} from "contracts/beanstalk/silo/WhitelistFacet/Whiteli * @notice Manages the Silo Whitelist including Adding to, Updating * and Removing from the Silo Whitelist **/ -contract WhitelistFacet is WhitelistedTokens { +contract WhitelistFacet is Invariable, WhitelistedTokens { /** * @notice Removes a token from the Silo Whitelist. * @dev Can only be called by Beanstalk or Beanstalk owner. */ - function dewhitelistToken(address token) external payable { + function dewhitelistToken(address token) external payable fundsSafu noNetFlow noSupplyChange { LibDiamond.enforceIsOwnerOrContract(); LibWhitelist.dewhitelistToken(token); } @@ -50,7 +51,7 @@ contract WhitelistFacet is WhitelistedTokens { bytes4 liquidityWeightSelector, uint128 gaugePoints, uint64 optimalPercentDepositedBdv - ) external payable { + ) external payable fundsSafu noNetFlow noSupplyChange { LibDiamond.enforceIsOwnerOrContract(); LibWhitelist.whitelistToken( token, @@ -89,7 +90,7 @@ contract WhitelistFacet is WhitelistedTokens { bytes4 liquidityWeightSelector, uint128 gaugePoints, uint64 optimalPercentDepositedBdv - ) external payable { + ) external payable fundsSafu noNetFlow noSupplyChange { LibDiamond.enforceIsOwnerOrContract(); LibWhitelist.whitelistToken( token, @@ -113,7 +114,7 @@ contract WhitelistFacet is WhitelistedTokens { function updateStalkPerBdvPerSeasonForToken( address token, uint32 stalkEarnedPerSeason - ) external payable { + ) external payable fundsSafu noNetFlow noSupplyChange { LibDiamond.enforceIsOwnerOrContract(); LibWhitelist.updateStalkPerBdvPerSeasonForToken(token, stalkEarnedPerSeason); } @@ -127,7 +128,7 @@ contract WhitelistFacet is WhitelistedTokens { bytes4 gaugePointSelector, bytes4 liquidityWeightSelector, uint64 optimalPercentDepositedBdv - ) external payable { + ) external payable fundsSafu noNetFlow noSupplyChange { LibDiamond.enforceIsOwnerOrContract(); LibWhitelist.updateGaugeForToken( token, diff --git a/protocol/contracts/beanstalk/sun/SeasonFacet/SeasonFacet.sol b/protocol/contracts/beanstalk/sun/SeasonFacet/SeasonFacet.sol index a5a10d59f0..cd5be5f180 100644 --- a/protocol/contracts/beanstalk/sun/SeasonFacet/SeasonFacet.sol +++ b/protocol/contracts/beanstalk/sun/SeasonFacet/SeasonFacet.sol @@ -10,6 +10,7 @@ import {LibWell} from "contracts/libraries/Well/LibWell.sol"; import {LibGauge} from "contracts/libraries/LibGauge.sol"; import {LibWhitelistedTokens} from "contracts/libraries/Silo/LibWhitelistedTokens.sol"; import {LibGerminate} from "contracts/libraries/Silo/LibGerminate.sol"; +import {Invariable} from "contracts/beanstalk/Invariable.sol"; import {LibTractor} from "contracts/libraries/LibTractor.sol"; /** @@ -17,7 +18,7 @@ import {LibTractor} from "contracts/libraries/LibTractor.sol"; * @author Publius, Chaikitty, Brean * @notice Holds the Sunrise function and handles all logic for Season changes. */ -contract SeasonFacet is Weather { +contract SeasonFacet is Invariable, Weather { using SafeMath for uint256; /** @@ -31,8 +32,10 @@ contract SeasonFacet is Weather { /** * @notice Advances Beanstalk to the next Season, sending reward Beans to the caller's circulating balance. * @return reward The number of beans minted to the caller. + * @dev No out flow because any externally sent reward beans are freshly minted. */ - function sunrise() external payable returns (uint256) { + // TODO: FIx this. should be broken from noNetFlow bc balance of beanstalk will increase + function sunrise() external payable fundsSafu noOutFlow returns (uint256) { return gm(LibTractor._user(), LibTransfer.To.EXTERNAL); } @@ -41,8 +44,12 @@ contract SeasonFacet is Weather { * @param account Indicates to which address reward Beans should be sent * @param mode Indicates whether the reward beans are sent to internal or circulating balance * @return reward The number of Beans minted to the caller. + * @dev No out flow because any externally sent reward beans are freshly minted. */ - function gm(address account, LibTransfer.To mode) public payable returns (uint256) { + function gm( + address account, + LibTransfer.To mode + ) public payable fundsSafu noOutFlow returns (uint256) { uint256 initialGasLeft = gasleft(); require(!s.paused, "Season: Paused."); diff --git a/protocol/contracts/beanstalk/sun/SeasonFacet/Weather.sol b/protocol/contracts/beanstalk/sun/SeasonFacet/Weather.sol index f5ac331e7f..2e27afbff0 100644 --- a/protocol/contracts/beanstalk/sun/SeasonFacet/Weather.sol +++ b/protocol/contracts/beanstalk/sun/SeasonFacet/Weather.sol @@ -214,6 +214,7 @@ contract Weather is Sun { address(this), type(uint256).max ); + s.plenty += amountOut; rewardSop(amountOut); emit SeasonOfPlenty( s.season.current, diff --git a/protocol/contracts/libraries/LibEvaluate.sol b/protocol/contracts/libraries/LibEvaluate.sol index 4b968a713b..a6e6b6ba53 100644 --- a/protocol/contracts/libraries/LibEvaluate.sol +++ b/protocol/contracts/libraries/LibEvaluate.sol @@ -234,7 +234,7 @@ library LibEvaluate { // if the liquidity is the largest, update `largestLiqWell`, // and add the liquidity to the total. - // `largestLiqWell` is only used to initalize `s.sopWell` upon a sop, + // `largestLiqWell` is only used to initialize `s.sopWell` upon a sop, // but a hot storage load to skip the block below // is significantly more expensive than performing the logic on every sunrise. if (wellLiquidity > largestLiq) { diff --git a/protocol/contracts/libraries/LibUnripe.sol b/protocol/contracts/libraries/LibUnripe.sol index c8eca1fdc1..34ba449816 100644 --- a/protocol/contracts/libraries/LibUnripe.sol +++ b/protocol/contracts/libraries/LibUnripe.sol @@ -237,4 +237,11 @@ library LibUnripe { AppStorage storage s = LibAppStorage.diamondStorage(); redeem = s.u[unripeToken].balanceOfUnderlying.mul(amount).div(supply); } + + function _getUnderlyingToken( + address unripeToken + ) internal view returns (address underlyingToken) { + AppStorage storage s = LibAppStorage.diamondStorage(); + return s.u[unripeToken].underlyingToken; + } } diff --git a/protocol/contracts/libraries/Silo/LibSilo.sol b/protocol/contracts/libraries/Silo/LibSilo.sol index 6c6aa1e027..c05475a916 100644 --- a/protocol/contracts/libraries/Silo/LibSilo.sol +++ b/protocol/contracts/libraries/Silo/LibSilo.sol @@ -17,6 +17,8 @@ import {LibSafeMathSigned96} from "../LibSafeMathSigned96.sol"; import {LibGerminate} from "./LibGerminate.sol"; import {LibWhitelistedTokens} from "./LibWhitelistedTokens.sol"; import {LibTractor} from "../LibTractor.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IWell} from "contracts/interfaces/basin/IWell.sol"; /** * @title LibSilo @@ -482,6 +484,19 @@ library LibSilo { return s.a[account].lastUpdate; } + // TODO: Monitor incompatibilities (esp wrt initialization) when generalized flood merges in. + /** + * @notice returns the token paid out by Season of Plenty. + */ + function getSopToken() internal view returns (IERC20) { + AppStorage storage s = LibAppStorage.diamondStorage(); + // sopWell may not yet be initialized. + if (s.sopWell == address(0)) return IERC20(address(0)); + IWell well = IWell(s.sopWell); + IERC20[] memory tokens = well.tokens(); + return tokens[0] != C.bean() ? tokens[0] : tokens[1]; + } + /** * @dev internal logic to handle when beanstalk is raining. */ diff --git a/protocol/contracts/libraries/Token/LibBalance.sol b/protocol/contracts/libraries/Token/LibBalance.sol index 9434d21e7c..f85b75846a 100644 --- a/protocol/contracts/libraries/Token/LibBalance.sol +++ b/protocol/contracts/libraries/Token/LibBalance.sol @@ -84,6 +84,13 @@ library LibBalance { int256 delta ) private { AppStorage storage s = LibAppStorage.diamondStorage(); + delta >= 0 + ? s.internalTokenBalanceTotal[token] = s.internalTokenBalanceTotal[token].add( + uint256(delta) + ) + : s.internalTokenBalanceTotal[token] = s.internalTokenBalanceTotal[token].sub( + uint256(-delta) + ); s.internalTokenBalance[account][token] = newBalance; emit InternalBalanceChanged(account, token, delta); } diff --git a/protocol/contracts/mocks/mockFacets/MockAdminFacet.sol b/protocol/contracts/mocks/mockFacets/MockAdminFacet.sol index 0f721d8eef..210a48bc8b 100644 --- a/protocol/contracts/mocks/mockFacets/MockAdminFacet.sol +++ b/protocol/contracts/mocks/mockFacets/MockAdminFacet.sol @@ -9,6 +9,8 @@ import "contracts/libraries/Token/LibTransfer.sol"; import "contracts/beanstalk/sun/SeasonFacet/SeasonFacet.sol"; import "contracts/beanstalk/sun/SeasonFacet/Sun.sol"; import {LibCurveMinting} from "contracts/libraries/Minting/LibCurveMinting.sol"; +import {LibTransfer} from "contracts/libraries/Token/LibTransfer.sol"; +import {LibBalance} from "contracts/libraries/Token/LibBalance.sol"; /** * @author Publius diff --git a/protocol/contracts/mocks/mockFacets/MockExploitFacet.sol b/protocol/contracts/mocks/mockFacets/MockExploitFacet.sol new file mode 100644 index 0000000000..67fde1ebeb --- /dev/null +++ b/protocol/contracts/mocks/mockFacets/MockExploitFacet.sol @@ -0,0 +1,107 @@ +/* + SPDX-License-Identifier: MIT +*/ +pragma solidity =0.7.6; +pragma experimental ABIEncoderV2; + +import "contracts/C.sol"; +import "contracts/libraries/Token/LibTransfer.sol"; +import {LibTransfer} from "contracts/libraries/Token/LibTransfer.sol"; +import {Invariable} from "contracts/beanstalk/Invariable.sol"; + +/** + * @author funderbrker + * @title MockExploitFacet provides artificial vulnerabilities for testing + **/ + +contract MockExploitFacet is Invariable { + AppStorage internal s; + + /* State checking */ + + function entitlementsMatchBalances() public view returns (bool) { + address[] memory tokens = getTokensOfInterest(); + ( + uint256[] memory entitlements, + uint256[] memory balances + ) = getTokenEntitlementsAndBalances(tokens); + + for (uint256 i = 0; i < tokens.length; i++) { + if (entitlements[i] != balances[i]) return false; + } + return true; + } + + /* Internal token accounting exploits. */ + + function exploitUserInternalTokenBalance() public fundsSafu { + LibBalance.increaseInternalBalance(msg.sender, IERC20(C.UNRIPE_LP), 100_000_000); + } + + function exploitUserSendTokenInternal() public fundsSafu { + LibTransfer.sendToken( + IERC20(C.BEAN_ETH_WELL), + 100_000_000_000, + msg.sender, + LibTransfer.To.INTERNAL + ); + } + + function exploitFertilizer() public fundsSafu { + s.fertilizedIndex += 100_000_000_000; + } + + function exploitSop(address sopWell) public fundsSafu { + s.sopWell = sopWell; + s.plenty = 100_000_000; + } + + /* Token flow exploits. */ + + function exploitTokenBalance() public noNetFlow { + C.bean().transferFrom(msg.sender, address(this), 1_000_000); + } + + function exploitUserSendTokenExternal0() public noNetFlow { + LibTransfer.sendToken(IERC20(C.BEAN), 10_000_000_000, msg.sender, LibTransfer.To.EXTERNAL); + } + + function exploitUserSendTokenExternal1() public noOutFlow { + LibTransfer.sendToken(IERC20(C.BEAN), 10_000_000_000, msg.sender, LibTransfer.To.EXTERNAL); + } + + function exploitUserDoubleSendTokenExternal() public oneOutFlow(C.BEAN) { + LibTransfer.sendToken(IERC20(C.BEAN), 10_000_000_000, msg.sender, LibTransfer.To.EXTERNAL); + LibTransfer.sendToken(IERC20(C.UNRIPE_LP), 10_000_000, msg.sender, LibTransfer.To.EXTERNAL); + } + + function exploitBurnStalk0() public noNetFlow { + s.s.stalk -= 1_000_000_000; + } + + function exploitBurnStalk1() public noOutFlow { + s.s.stalk -= 1_000_000_000; + } + + /* Bean supply exploits. */ + + function exploitBurnBeans() public noSupplyChange { + C.bean().burn(100_000_000); + } + + function exploitMintBeans0() public noSupplyChange { + C.bean().mint(msg.sender, 100_000_000); + } + + function exploitMintBeans1() public noSupplyChange { + C.bean().mint(address(this), 100_000_000); + } + + function exploitMintBeans2() public noSupplyIncrease { + C.bean().mint(msg.sender, 100_000_000); + } + + function exploitMintBeans3() public noSupplyIncrease { + C.bean().mint(address(this), 100_000_000); + } +} diff --git a/protocol/contracts/mocks/mockFacets/MockFertilizerFacet.sol b/protocol/contracts/mocks/mockFacets/MockFertilizerFacet.sol index 602c9435f8..c850f7cc22 100644 --- a/protocol/contracts/mocks/mockFacets/MockFertilizerFacet.sol +++ b/protocol/contracts/mocks/mockFacets/MockFertilizerFacet.sol @@ -25,6 +25,7 @@ contract MockFertilizerFacet is FertilizerFacet { function setPenaltyParams(uint256 recapitalized, uint256 fertilized) external { s.recapitalized = recapitalized; s.fertilizedIndex = fertilized; + s.fertilizedPaidIndex = fertilized; } function setFertilizerE(bool fertilizing, uint256 unfertilized) external { diff --git a/protocol/contracts/mocks/mockFacets/MockUnripeFacet.sol b/protocol/contracts/mocks/mockFacets/MockUnripeFacet.sol index 04d5552725..94c5177720 100644 --- a/protocol/contracts/mocks/mockFacets/MockUnripeFacet.sol +++ b/protocol/contracts/mocks/mockFacets/MockUnripeFacet.sol @@ -35,4 +35,8 @@ contract MockUnripeFacet is UnripeFacet { IERC20(underlyingToken).safeTransferFrom(LibTractor._user(), address(this), amount); LibUnripe.addUnderlying(unripeToken, amount); } + + function resetUnderlying(address unripeToken) external { + s.u[unripeToken].balanceOfUnderlying = 0; + } } diff --git a/protocol/scripts/deploy.js b/protocol/scripts/deploy.js index b70bf9672a..f3bb08f433 100644 --- a/protocol/scripts/deploy.js +++ b/protocol/scripts/deploy.js @@ -89,9 +89,9 @@ async function main( const initDiamondArg = mock ? "contracts/mocks/newMockInitDiamond.sol:MockInitDiamond" : "contracts/beanstalk/init/newInitDiamond.sol:InitDiamond"; - // eslint-disable-next-line no-unused-vars - + + // eslint-disable-next-line no-unused-vars // Impersonate various contracts that beanstalk interacts with. // These should be impersonated on a fresh network state. let basinComponents = [] diff --git a/protocol/test/DepotFacet.test.js b/protocol/test/DepotFacet.test.js index cbed34421c..d8212b5025 100644 --- a/protocol/test/DepotFacet.test.js +++ b/protocol/test/DepotFacet.test.js @@ -22,7 +22,7 @@ let user, user2, owner; describe("Depot Facet", function () { before(async function () { - [owner, user, user2] = await ethers.getSigners(); + [owner, user, user2, user3] = await ethers.getSigners(); const contracts = await deploy((verbose = false), (mock = true), (reset = true)); this.diamond = contracts.beanstalkDiamond.address; // `beanstalk` contains all functions that the regualar beanstalk has. @@ -60,18 +60,28 @@ describe("Depot Facet", function () { describe("Normal Pipe", async function () { describe("1 Pipe", async function () { beforeEach(async function () { - const mintBeans = bean.interface.encodeFunctionData("mint", [pipeline.address, to6("100")]); - await beanstalk.connect(user).pipe([bean.address, mintBeans]); + expect(await bean.balanceOf(user3.address)).to.be.equal(to6("0")); + + await bean.mint(pipeline.address, to6("100")); + const transferBeans = bean.interface.encodeFunctionData("transfer", [ + user3.address, + to6("100") + ]); + await beanstalk.connect(user).pipe([bean.address, transferBeans]); }); - it("mints beans", async function () { - expect(await bean.balanceOf(pipeline.address)).to.be.equal(to6("100")); + it("erc20 transfer beans", async function () { + expect(await bean.balanceOf(user3.address)).to.be.equal(to6("100")); }); }); describe("Multi Pipe", async function () { beforeEach(async function () { - const mintBeans = bean.interface.encodeFunctionData("mint", [pipeline.address, to6("100")]); + expect(await beanstalk.getInternalBalance(user.address, bean.address)).to.be.equal( + to6("0") + ); + + await bean.mint(pipeline.address, to6("100")); const approve = await bean.interface.encodeFunctionData("approve", [ beanstalk.address, to6("100") @@ -84,13 +94,12 @@ describe("Depot Facet", function () { 1 ]); await beanstalk.connect(user).multiPipe([ - [bean.address, mintBeans], [bean.address, approve], [beanstalk.address, tokenTransfer] ]); }); - it("mints and transfers beans", async function () { + it("approves and transfers beans via beanstalk", async function () { expect(await beanstalk.getInternalBalance(user.address, bean.address)).to.be.equal( to6("100") ); diff --git a/protocol/test/Invariable.test.js b/protocol/test/Invariable.test.js new file mode 100644 index 0000000000..55a01e42b4 --- /dev/null +++ b/protocol/test/Invariable.test.js @@ -0,0 +1,152 @@ +const { expect } = require("chai"); +const { deploy } = require("../scripts/deploy.js"); +const { upgradeWithNewFacets } = require("../scripts/diamond"); +const { impersonateBeanstalkOwner, impersonateSigner } = require("../utils/signer.js"); +const { mintEth, mintBeans } = require("../utils/mint.js"); +const { EXTERNAL, INTERNAL } = require("./utils/balances.js"); +const { + BEAN, + MAX_UINT256, + BEAN_ETH_WELL, + WETH, + UNRIPE_BEAN, + UNRIPE_LP, + ZERO_BYTES +} = require("./utils/constants"); +const { setEthUsdChainlinkPrice, setWstethUsdPrice } = require("../utils/oracle.js"); +const { deployMockWellWithMockPump } = require("../utils/well.js"); +const { to6, to18 } = require("./utils/helpers.js"); +const { + initalizeUsersForToken, + endGermination, + setRecapitalizationParams +} = require("./utils/testHelpers.js"); + +const { getAllBeanstalkContracts } = require("../utils/contracts"); + +let user, user2, owner; + +describe("Invariants", function () { + before(async function () { + [owner, user, user2] = await ethers.getSigners(); + + const contracts = await deploy((verbose = false), (mock = true), (reset = true)); + this.diamond = contracts.beanstalkDiamond; + + bean = await ethers.getContractAt("MockToken", BEAN); + + [beanstalk, mockBeanstalk] = await getAllBeanstalkContracts(this.diamond.address); + + owner = await impersonateBeanstalkOwner(); + await mintEth(owner.address); + await upgradeWithNewFacets({ + diamondAddress: this.diamond.address, + facetNames: ["MockExploitFacet"], + bip: false, + object: false, + verbose: false, + account: owner + }); + + // Set up Wells. + [this.well, this.wellFunction, this.pump] = await deployMockWellWithMockPump(); + await this.well.setReserves([to6("1000000"), to18("1000")]); + // await this.well.connect(user).mint(user.address, to18('1000')) + + // Initialize users - mint bean and approve beanstalk to use all beans. + await initalizeUsersForToken(BEAN, [user, user2, owner], to6("1000000")); + await initalizeUsersForToken(BEAN_ETH_WELL, [user, user2, owner], to18("10000")); + await initalizeUsersForToken(WETH, [user, user2, owner], to18("10000")); + await initalizeUsersForToken(UNRIPE_BEAN, [user, user2, owner], to6("10000")); + await initalizeUsersForToken(UNRIPE_LP, [user, user2, owner], to6("10000")); + + // Set up unripes. + this.unripeBean = await ethers.getContractAt("MockToken", UNRIPE_BEAN); + this.unripeLP = await ethers.getContractAt("MockToken", UNRIPE_LP); + await this.unripeLP.mint(user.address, to6("10000")); + await this.unripeLP.connect(user).approve(this.diamond.address, MAX_UINT256); + await this.unripeBean.mint(user.address, to6("10000")); + await this.unripeBean.connect(user).approve(this.diamond.address, MAX_UINT256); + await mockBeanstalk.setFertilizerE(true, to6("10000")); + await mockBeanstalk.addUnripeToken(UNRIPE_BEAN, BEAN, ZERO_BYTES); + await mockBeanstalk.connect(owner).addUnderlying(UNRIPE_BEAN, to6("10000")); + await mockBeanstalk.addUnripeToken(UNRIPE_LP, BEAN_ETH_WELL, ZERO_BYTES); + await mockBeanstalk.connect(owner).addUnderlying(UNRIPE_LP, to6("10000")); + await setRecapitalizationParams(owner); + + const whitelist = await ethers.getContractAt( + "WhitelistFacet", + contracts.beanstalkDiamond.address + ); + + // Set up Field. + await mockBeanstalk.incrementTotalSoilE(to6("1000")); + + await setEthUsdChainlinkPrice("1000"); + await setWstethUsdPrice("1001"); + + // Deposits tokens from 2 users. + expect(await mockBeanstalk.entitlementsMatchBalances()).true; + await beanstalk.connect(user).deposit(BEAN, to6("2000"), EXTERNAL); + expect(await mockBeanstalk.entitlementsMatchBalances()).true; + await beanstalk.connect(user).deposit(BEAN, to6("3000"), EXTERNAL); + expect(await mockBeanstalk.entitlementsMatchBalances()).true; + await beanstalk.connect(user).deposit(UNRIPE_BEAN, to6("6000"), EXTERNAL); + expect(await mockBeanstalk.entitlementsMatchBalances()).true; + await beanstalk.connect(user).deposit(UNRIPE_LP, to6("7000"), EXTERNAL); + expect(await mockBeanstalk.entitlementsMatchBalances()).true; + + // With the germination update, the users deposit will not be active until the remainder of the season + 1 has passed. + await endGermination(); + expect(await mockBeanstalk.entitlementsMatchBalances()).true; + }); + + describe("Reverts exploits", async function () { + it("reverts at internal accounting exploit", async function () { + await expect(mockBeanstalk.exploitUserInternalTokenBalance()).to.be.revertedWith( + "INV: Insufficient token balance" + ); + await expect(mockBeanstalk.exploitUserSendTokenInternal()).to.be.revertedWith( + "INV: Insufficient token balance" + ); + await expect(mockBeanstalk.exploitFertilizer()).to.be.revertedWith( + "INV: Insufficient token balance" + ); + mockBeanstalk.mockSetSopWell(BEAN_ETH_WELL); + await expect(mockBeanstalk.exploitSop(this.well.address)).to.be.revertedWith( + "INV: Insufficient token balance" + ); + }); + + it("reverts at token flow exploit", async function () { + await expect(mockBeanstalk.exploitTokenBalance()).to.be.revertedWith( + "INV: noNetFlow Token balance changed" + ); + await expect(mockBeanstalk.exploitUserSendTokenExternal0()).to.be.revertedWith( + "INV: noNetFlow Token balance changed" + ); + await expect(mockBeanstalk.exploitUserSendTokenExternal1()).to.be.revertedWith( + "INV: noOutFlow Token balance decreased" + ); + await expect(mockBeanstalk.exploitUserDoubleSendTokenExternal()).to.be.revertedWith( + "INV: oneOutFlow multiple token balances decreased" + ); + await expect(mockBeanstalk.exploitBurnStalk0()).to.be.revertedWith( + "INV: noNetFlow Stalk decreased" + ); + await expect(mockBeanstalk.exploitBurnStalk1()).to.be.revertedWith( + "INV: noOutFlow Stalk decreased" + ); + }); + + it("reverts at supply exploit", async function () { + await expect(mockBeanstalk.exploitBurnBeans()).to.be.revertedWith("INV: Supply changed"); + await expect(mockBeanstalk.exploitMintBeans0()).to.be.revertedWith("INV: Supply changed"); + await expect(mockBeanstalk.exploitMintBeans1()).to.be.revertedWith("INV: Supply changed"); + await expect(mockBeanstalk.exploitMintBeans2()).to.be.revertedWith("INV: Supply increased"); + await expect(mockBeanstalk.exploitMintBeans3()).to.be.revertedWith("INV: Supply increased"); + }); + + // if("tracks SOP token", async function () {}) + }); +}); diff --git a/protocol/test/SiloEnroot.test.js b/protocol/test/SiloEnroot.test.js index d47124b7e5..829d1450b5 100644 --- a/protocol/test/SiloEnroot.test.js +++ b/protocol/test/SiloEnroot.test.js @@ -45,7 +45,7 @@ describe("Silo Enroot", function () { [owner, user, user2] = await ethers.getSigners(); // Setup mock facets for manipulating Beanstalk's state during tests - const contracts = await deploy((verbose = false), (mock = true), (reset = true)); + const contracts = await deploy(verbose = false, mock = true, reset = true); ownerAddress = contracts.account; this.diamond = contracts.beanstalkDiamond; // `beanstalk` contains all functions that the regualar beanstalk has. @@ -68,6 +68,12 @@ describe("Silo Enroot", function () { "1" ); + this.beanWstethWell = await ethers.getContractAt("MockToken", BEAN_WSTETH_WELL); + + // Needed to appease invariants when underlying asset of urBean != Bean. + await mockBeanstalk.removeWhitelistStatus(BEAN); + + await mockBeanstalk.teleportSunrise(ENROOT_FIX_SEASON); [this.well, this.wellfunction, this.pump] = await deployMockWellWithMockPump( BEAN_WSTETH_WELL, @@ -305,6 +311,9 @@ describe("Silo Enroot", function () { describe("2 deposit, round", async function () { beforeEach(async function () { + // Bypass fundsSafu invariant because this test handling of underlying tokens violates operating conditions. + await this.beanWstethWell.mint(mockBeanstalk.address, to18("10000")); + await mockBeanstalk.connect(owner).addUnderlying(UNRIPE_LP, "147796000000000"); await mockBeanstalk.connect(owner).addUnripeToken(UNRIPE_LP, BEAN_WSTETH_WELL, ZERO_BYTES); await mockBeanstalk diff --git a/protocol/test/SiloToken.test.js b/protocol/test/SiloToken.test.js index 24bfddeb8d..1f9d846a55 100644 --- a/protocol/test/SiloToken.test.js +++ b/protocol/test/SiloToken.test.js @@ -56,6 +56,9 @@ describe("New Silo Token", function () { 1e6 //aka "1 seed" ); + // Needed to appease invariants when underlying asset of urBean != Bean. + await mockBeanstalk.removeWhitelistStatus(BEAN); + await initalizeUsersForToken( siloToken.address, [user, user2, owner, flashLoanExploiter], diff --git a/protocol/test/Sop.test.js b/protocol/test/Sop.test.js index 3353ef852e..436e05e68a 100644 --- a/protocol/test/Sop.test.js +++ b/protocol/test/Sop.test.js @@ -201,14 +201,15 @@ describe("Sop", function () { it("claims user plenty", async function () { await beanstalk.mow(user2.address, this.well.address); await beanstalk.connect(user2).claimPlenty(); - expect(await beanstalk.balanceOfPlenty(user2.address)).to.be.equal("0"); - expect(await this.weth.balanceOf(user2.address)).to.be.equal(to18("25.595575914848452999")); - }); - - it("changes the sop well", async function () { - expect(await beanstalk.getSopWell()).to.be.equal(this.well.address); - }); - }); + expect(await beanstalk.balanceOfPlenty(user2.address)).to.be.equal('0') + expect(await this.weth.balanceOf(user2.address)).to.be.equal(to18('25.595575914848452999')) + }) + + it('changes the sop well', async function () { + expect(await beanstalk.getSopWell()).to.not.be.equal(ZERO_ADDRESS) + expect(await beanstalk.getSopWell()).to.be.equal(this.well.address) + }) + }) describe("multiple sop", async function () { beforeEach(async function () { @@ -336,12 +337,13 @@ describe("Sop", function () { it("claims user plenty", async function () { await beanstalk.mow(user2.address, this.well.address); await beanstalk.connect(user2).claimPlenty(); - expect(await beanstalk.balanceOfPlenty(user2.address)).to.be.equal("0"); - expect(await this.weth.balanceOf(user2.address)).to.be.equal(to18("25.595575914848452999")); - }); - - it("changes the sop well", async function () { - expect(await beanstalk.getSopWell()).to.be.equal(this.well.address); - }); - }); -}); + expect(await beanstalk.balanceOfPlenty(user2.address)).to.be.equal('0') + expect(await this.weth.balanceOf(user2.address)).to.be.equal(to18('25.595575914848452999')) + }) + + it('changes the sop well', async function () { + expect(await beanstalk.getSopWell()).to.not.be.equal(ZERO_ADDRESS) + expect(await beanstalk.getSopWell()).to.be.equal(this.well.address) + }) + }) +}) diff --git a/protocol/test/Sun.test.js b/protocol/test/Sun.test.js index 4b63e1b75b..80da3a1da2 100644 --- a/protocol/test/Sun.test.js +++ b/protocol/test/Sun.test.js @@ -273,11 +273,10 @@ describe("Sun", function () { [[to6("10000"), to18("6.66666")], 50 * Math.pow(10, 9), 24, INTERNAL], [[to6("10000"), to18("6.666666")], 50 * Math.pow(10, 9), 500, INTERNAL] ]; - let START_TIME = (await ethers.provider.getBlock("latest")).timestamp; - await timeSkip(START_TIME + 60 * 60 * 3); - // Load some beans into the wallet's internal balance, and note the starting time + let START_TIME = (await ethers.provider.getBlock('latest')).timestamp; + await timeSkip(START_TIME + 60*60*3); // This also accomplishes initializing curve oracle - const initial = await beanstalk.gm(owner.address, INTERNAL); + const initial = await beanstalk.connect(owner).sunrise(); const block = await ethers.provider.getBlock(initial.blockNumber); START_TIME = (await ethers.provider.getBlock("latest")).timestamp; await mockBeanstalk.setCurrentSeasonE(1); @@ -301,8 +300,12 @@ describe("Sun", function () { await mockBeanstalk.resetSeasonStart(secondsLate); // SUNRISE - this.result = await beanstalk.gm(owner.address, mockVal[3]); - + if (mockVal[3] == EXTERNAL) { + this.result = await beanstalk.sunrise(); + } else { + this.result = await beanstalk.gm(owner.address, mockVal[3]); + } + // Verify that sunrise was profitable assuming a 50% average success rate const beanBalance = diff --git a/protocol/test/UnripeBdvRemoval.test.js b/protocol/test/UnripeBdvRemoval.test.js index 30d41906e5..3350df0379 100644 --- a/protocol/test/UnripeBdvRemoval.test.js +++ b/protocol/test/UnripeBdvRemoval.test.js @@ -79,6 +79,9 @@ describe("Silo Enroot", function () { await this.siloToken.connect(owner).approve(beanstalk.address, to18("10000")); await this.siloToken.mint(ownerAddress, to18("10000")); + // Needed to appease invariants when underlying asset of urBean != Bean. + await mockBeanstalk.removeWhitelistStatus(BEAN); + this.unripeBeans = await ethers.getContractAt("MockToken", UNRIPE_BEAN); await this.unripeBeans.connect(user).mint(user.address, to6("10000")); await this.unripeBeans.connect(user).approve(beanstalk.address, to18("10000"));