Merge pull request 'fix: CREATE2 self-destruct bypass in onchain/src/LiquidityManager.sol (#921)' (#957) from fix/issue-921 into master
This commit is contained in:
commit
feff600dbd
2 changed files with 58 additions and 12 deletions
|
|
@ -112,23 +112,36 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Sets the fee destination address (deployer only).
|
/// @notice Sets the fee destination address (deployer only).
|
||||||
/// @dev Conditional trapdoor: EOA addresses can be set repeatedly (allows setup/migration),
|
/// @dev Trapdoor lock: EOA addresses can be set repeatedly (enables staged deployment/migration)
|
||||||
/// but once a contract address is assigned, feeDestinationLocked is set and no further
|
/// until the lock is triggered. The lock fires in two ways:
|
||||||
/// changes are allowed. Note: the deployer retains fee-redirect ability while feeDestination
|
|
||||||
/// is an EOA; this is an intentional trade-off that enables staged deployment.
|
|
||||||
///
|
///
|
||||||
/// CREATE2 guard: also blocks re-assignment if the current feeDestination has since
|
/// 1. Direct assignment: setting feeDestination to a contract address (code.length > 0)
|
||||||
/// acquired bytecode (i.e. was a pre-computed EOA when set, later deployed to).
|
/// immediately and permanently locks further changes. This is the expected production
|
||||||
|
/// path — set an EOA during development, upgrade to a treasury contract when ready.
|
||||||
|
///
|
||||||
|
/// 2. Defensive CREATE2 guard: if the current feeDestination was an EOA when set but has
|
||||||
|
/// since acquired bytecode (e.g. via CREATE2 deployment), the next call to this
|
||||||
|
/// function permanently sets feeDestinationLocked = true and returns WITHOUT reverting.
|
||||||
|
/// Not reverting is intentional: only a successful transaction commits the lock to
|
||||||
|
/// storage, so the lock survives a subsequent SELFDESTRUCT that would otherwise clear
|
||||||
|
/// the bytecode evidence and re-open the assignment window.
|
||||||
|
///
|
||||||
|
/// Remaining limitation: an atomic CREATE2+SELFDESTRUCT within a single transaction
|
||||||
|
/// (still possible post-EIP-6780 for contracts that deploy and destroy in one call)
|
||||||
|
/// cannot be detected here because the bytecode is already absent when this function
|
||||||
|
/// executes. No on-chain guard can close that sub-transaction window.
|
||||||
/// @param feeDestination_ The address that will receive trading fees
|
/// @param feeDestination_ The address that will receive trading fees
|
||||||
function setFeeDestination(address feeDestination_) external {
|
function setFeeDestination(address feeDestination_) external {
|
||||||
require(msg.sender == deployer, "only deployer");
|
require(msg.sender == deployer, "only deployer");
|
||||||
if (address(0) == feeDestination_) revert ZeroAddressInSetter();
|
if (address(0) == feeDestination_) revert ZeroAddressInSetter();
|
||||||
// Block if explicitly locked OR if the current destination has since become a contract
|
// Defensive CREATE2 guard: if the current destination has acquired bytecode since being
|
||||||
// (guards CREATE2 bypass: address looked like an EOA when set, bytecode deployed later)
|
// set as an EOA, lock permanently and return WITHOUT reverting so the write is committed
|
||||||
require(
|
// to storage. A subsequent SELFDESTRUCT clears the bytecode but cannot undo this write.
|
||||||
!feeDestinationLocked && (feeDestination == address(0) || feeDestination.code.length == 0),
|
if (!feeDestinationLocked && feeDestination != address(0) && feeDestination.code.length > 0) {
|
||||||
"fee destination locked"
|
feeDestinationLocked = true;
|
||||||
);
|
return;
|
||||||
|
}
|
||||||
|
require(!feeDestinationLocked, "fee destination locked");
|
||||||
feeDestination = feeDestination_;
|
feeDestination = feeDestination_;
|
||||||
if (feeDestination_.code.length > 0) {
|
if (feeDestination_.code.length > 0) {
|
||||||
feeDestinationLocked = true;
|
feeDestinationLocked = true;
|
||||||
|
|
|
||||||
|
|
@ -1023,6 +1023,39 @@ contract LiquidityManagerTest is UniSwapHelper {
|
||||||
freshLm.setFeeDestination(makeAddr("anyAddr"));
|
freshLm.setFeeDestination(makeAddr("anyAddr"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice CREATE2 bypass defense: if the current feeDestination acquires bytecode after being
|
||||||
|
* set as an EOA, the next setFeeDestination call commits the lock WITHOUT reverting so
|
||||||
|
* the lock persists even after a subsequent SELFDESTRUCT clears the bytecode.
|
||||||
|
*/
|
||||||
|
function testSetFeeDestination_CREATE2BytecodeDetection_Locks() public {
|
||||||
|
LiquidityManager freshLm = new LiquidityManager(address(factory), address(weth), address(harberg), address(optimizer));
|
||||||
|
address eoaAddr = makeAddr("precomputedEOA");
|
||||||
|
|
||||||
|
// Step 1: set to EOA — allowed, lock stays false
|
||||||
|
freshLm.setFeeDestination(eoaAddr);
|
||||||
|
assertFalse(freshLm.feeDestinationLocked(), "not locked after EOA set");
|
||||||
|
|
||||||
|
// Step 2: simulate CREATE2 deploy — bytecode now exists at the fee destination address
|
||||||
|
vm.etch(eoaAddr, hex"00");
|
||||||
|
assertGt(eoaAddr.code.length, 0, "precondition: address has code");
|
||||||
|
|
||||||
|
// Step 3: calling setFeeDestination detects bytecode at the current destination and
|
||||||
|
// commits feeDestinationLocked = true WITHOUT reverting, so the write survives
|
||||||
|
// a later SELFDESTRUCT. feeDestination itself is not changed.
|
||||||
|
freshLm.setFeeDestination(makeAddr("attacker"));
|
||||||
|
assertTrue(freshLm.feeDestinationLocked(), "locked after bytecode detected at current feeDestination");
|
||||||
|
assertEq(freshLm.feeDestination(), eoaAddr, "feeDestination must not change when defensive lock triggers");
|
||||||
|
|
||||||
|
// Step 4: simulate SELFDESTRUCT — bytecode gone, but storage lock persists
|
||||||
|
vm.etch(eoaAddr, hex"");
|
||||||
|
assertEq(eoaAddr.code.length, 0, "code cleared (selfdestruct simulation)");
|
||||||
|
|
||||||
|
// Step 5: re-assignment must still be blocked despite cleared bytecode
|
||||||
|
vm.expectRevert("fee destination locked");
|
||||||
|
freshLm.setFeeDestination(makeAddr("attacker"));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @notice When optimizer reverts, the catch block uses default bear params (covers lines 192-201)
|
* @notice When optimizer reverts, the catch block uses default bear params (covers lines 192-201)
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue