Skip to content

Commit

Permalink
Secure Beanstalk Invariants (#821)
Browse files Browse the repository at this point in the history
  • Loading branch information
funderbrker authored Apr 30, 2024
2 parents 9e1a121 + 1e41af4 commit 3dec045
Show file tree
Hide file tree
Showing 39 changed files with 953 additions and 150 deletions.
1 change: 1 addition & 0 deletions projects/sdk/src/lib/silo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
137 changes: 137 additions & 0 deletions protocol/abi/MockBeanstalk.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down Expand Up @@ -10236,6 +10360,19 @@
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "unripeToken",
"type": "address"
}
],
"name": "resetUnderlying",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
Expand Down
7 changes: 7 additions & 0 deletions protocol/contracts/beanstalk/AppStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
195 changes: 195 additions & 0 deletions protocol/contracts/beanstalk/Invariable.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit 3dec045

Please sign in to comment.