Peg maintenance of the Bean price is achieved in several ways depending on the specific state of the protocol. During a flood, the bean price is above the peg value and there is an excessive low debt level. To address this, Beanstalk mints additional Beans. However, the calculation of the Beans to be minted can be compromised due to an incorrect implementation of the array sorting function.
Nevertheless, the scheme used to get the pivot does not take into account the following two cases:
This results in poor pivot selection.
The following code is a very detailed test of the quickSort function in order to illustrate the problems encountered. This code works as a standalone foundry contract test as it does not depend on external files or additional information. Executing it with a verbosity level of two (-vv
) is enough to see the logged explanation of the outputs.
pragma solidity >=0.6.0 <0.9.0;
import {Test, console} from "forge-std/Test.sol";
struct WellDeltaB {
address well;
int256 deltaB;
}
contract quickSortTest is Test {
WellDeltaB[] public arr0;
WellDeltaB[] public expectedOutputArray;
WellDeltaB public expectedPivot;
QuickSort sortArray;
function setUp() public {
sortArray = new QuickSort();
arr0.push(WellDeltaB(address(1), int256(39)));
arr0.push(WellDeltaB(address(2), int256(6)));
arr0.push(WellDeltaB(address(3), int256(27)));
arr0.push(WellDeltaB(address(4), int256(-14)));
arr0.push(WellDeltaB(address(5), int256(15)));
expectedPivot.well = address(3);
expectedPivot.deltaB = 27;
expectedOutputArray.push(WellDeltaB(address(1), int256(39)));
expectedOutputArray.push(WellDeltaB(address(3), int256(27)));
expectedOutputArray.push(WellDeltaB(address(5), int256(15)));
expectedOutputArray.push(WellDeltaB(address(2), int256(6)));
expectedOutputArray.push(WellDeltaB(address(4), int256(-14)));
}
function testPivotCalculation() public view {
WellDeltaB memory pivot1;
WellDeltaB memory pivot2;
pivot1 = sortArray.pivotOriginal(arr0, 0, int256(arr0.length - 1));
pivot2 = sortArray.pivot2(arr0, 0, int256(arr0.length - 1));
console.log("Expected pivot (1st iteration): %i", expectedPivot.deltaB);
console.log("Original pivot (1st iteration): %i", pivot1.deltaB);
console.log("Corrected pivot (1st iteration): %i", pivot2.deltaB);
assert(pivot1.deltaB != expectedPivot.deltaB);
assert(pivot2.deltaB == expectedPivot.deltaB);
}
function testQuickSortFunction() public view {
WellDeltaB[] memory arr1;
WellDeltaB[] memory arr2;
arr1 = sortArray.quickSort(arr0, 0, int256(arr0.length - 1));
arr2 = sortArray.quickSort2(arr0, 0, int256(arr0.length - 1));
console.log("Original array deltaBs:");
for (uint i; i < arr0.length; i++) {
console.log("deltaB: %i", arr0[i].deltaB);
}
console.log("\nSorted array using the original quickSort function:");
for (uint i; i < arr1.length; i++) {
console.log("deltaB: %i", arr1[i].deltaB);
}
console.log("\nSorted array using the corrected quickSort function:");
for (uint i; i < arr2.length; i++) {
console.log("deltaB: %i", arr2[i].deltaB);
}
bool quickSortSuccess = sortArray.compareArrays(
expectedOutputArray,
arr1
);
bool quickSort2Success = sortArray.compareArrays(
expectedOutputArray,
arr2
);
assert(quickSortSuccess == false);
assert(quickSort2Success == true);
}
}
contract QuickSort {
function quickSort(
WellDeltaB[] memory arr,
int left,
int right
) public pure returns (WellDeltaB[] memory) {
if (left >= right) return arr;
uint mid = uint(left) + (uint(right) - uint(left)) / 2;
WellDeltaB memory pivot = arr[uint(left)].deltaB > arr[uint(mid)].deltaB
? (
arr[uint(left)].deltaB < arr[uint(right)].deltaB
? arr[uint(left)]
: arr[uint(right)]
)
: (
arr[uint(mid)].deltaB < arr[uint(right)].deltaB
? arr[uint(mid)]
: arr[uint(right)]
);
int i = left;
int j = right;
while (i <= j) {
while (arr[uint(i)].deltaB > pivot.deltaB) i++;
while (pivot.deltaB > arr[uint(j)].deltaB) j--;
if (i <= j) {
(arr[uint(i)], arr[uint(j)]) = (arr[uint(j)], arr[uint(i)]);
i++;
j--;
}
}
if (left < j) {
return quickSort(arr, left, j);
}
if (i < right) {
return quickSort(arr, i, right);
}
return arr;
}
function quickSort2(
WellDeltaB[] memory arr,
int left,
int right
) public pure returns (WellDeltaB[] memory) {
if (left >= right) return arr;
uint mid = uint(left) + (uint(right) - uint(left)) / 2;
WellDeltaB memory pivot;
if (arr[uint(left)].deltaB > arr[uint(mid)].deltaB) {
if (arr[uint(left)].deltaB < arr[uint(right)].deltaB) {
pivot = arr[uint(left)];
} else {
if (arr[uint(right)].deltaB > arr[uint(mid)].deltaB) {
pivot = arr[uint(right)];
} else {
pivot = arr[uint(mid)];
}
}
} else {
if (arr[uint(mid)].deltaB < arr[uint(right)].deltaB) {
pivot = arr[uint(mid)];
} else {
if (arr[uint(right)].deltaB > arr[uint(left)].deltaB) {
pivot = arr[uint(right)];
} else {
pivot = arr[uint(left)];
}
}
}
int i = left;
int j = right;
while (i <= j) {
while (arr[uint(i)].deltaB > pivot.deltaB) i++;
while (pivot.deltaB > arr[uint(j)].deltaB) j--;
if (i <= j) {
(arr[uint(i)], arr[uint(j)]) = (arr[uint(j)], arr[uint(i)]);
i++;
j--;
}
}
if (left < j) {
arr = quickSort2(arr, left, j);
}
if (i < right) {
return quickSort2(arr, i, right);
}
return arr;
}
function pivotOriginal(
WellDeltaB[] memory arr,
int left,
int right
) public pure returns (WellDeltaB memory) {
uint mid = uint(left) + (uint(right) - uint(left)) / 2;
WellDeltaB memory pivot = arr[uint(left)].deltaB > arr[uint(mid)].deltaB
? (
arr[uint(left)].deltaB < arr[uint(right)].deltaB
? arr[uint(left)]
: arr[uint(right)]
)
: (
arr[uint(mid)].deltaB < arr[uint(right)].deltaB
? arr[uint(mid)]
: arr[uint(right)]
);
return pivot;
}
function pivot2(
WellDeltaB[] memory arr,
int left,
int right
) public pure returns (WellDeltaB memory) {
uint mid = uint(left) + (uint(right) - uint(left)) / 2;
WellDeltaB memory pivot;
if (arr[uint(left)].deltaB > arr[uint(mid)].deltaB) {
if (arr[uint(left)].deltaB < arr[uint(mid)].deltaB) {
pivot = arr[uint(left)];
} else {
if (arr[uint(right)].deltaB > arr[uint(mid)].deltaB) {
pivot = arr[uint(right)];
} else {
pivot = arr[uint(mid)];
}
}
} else {
if (arr[uint(mid)].deltaB < arr[uint(right)].deltaB) {
pivot = arr[uint(mid)];
} else {
if (arr[uint(right)].deltaB > arr[uint(left)].deltaB) {
pivot = arr[uint(right)];
} else {
pivot = arr[uint(left)];
}
}
}
return pivot;
}
function compareArrays(
WellDeltaB[] memory arr1,
WellDeltaB[] memory arr2
) public pure returns (bool) {
if (arr1.length != arr2.length) {
return false;
}
for (uint i = 0; i < arr1.length; i++) {
if (
arr1[i].well != arr2[i].well || arr1[i].deltaB != arr2[i].deltaB
) {
return false;
}
}
return true;
}
}
This bug directly affects the issuance of Beans in order to maintain the price peg. Its impact is directly proportional to the size of the wellDeltaB
array.
Only presented during a flood.
Previously presented in the proof of concept, it is advisable to change the scheme to obtain the pivot.
To include all the possible cases, an if-else
conditional statement scheme is recommended: