Skip to content

Commit 8875f52

Browse files
authored
✨ safeMoveETH (#1472)
1 parent f222647 commit 8875f52

File tree

2 files changed

+177
-0
lines changed

2 files changed

+177
-0
lines changed

src/utils/SafeTransferLib.sol

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ library SafeTransferLib {
6363
/// [Etherscan](https://etherscan.io/address/0x000000000022D473030F116dDEE9F6B43aC78BA3)
6464
address internal constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
6565

66+
/// @dev The canonical address of the `SELFDESTRUCT` ETH mover.
67+
/// See: https://gist.github.com/Vectorized/1cb8ad4cf393b1378e08f23f79bd99fa
68+
/// [Etherscan](https://etherscan.io/address/0x00000000000073c48c8055bD43D1A53799176f0D)
69+
address internal constant ETH_MOVER = 0x00000000000073c48c8055bD43D1A53799176f0D;
70+
6671
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
6772
/* ETH OPERATIONS */
6873
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
@@ -192,6 +197,46 @@ library SafeTransferLib {
192197
}
193198
}
194199

200+
/// @dev Force transfers ETH to `to`, without triggering the fallback (if any).
201+
/// This method attempts to use a separate contract to send via `SELFDESTRUCT`,
202+
/// and upon failure, deploys a minimal vault to accrue the ETH.
203+
function safeMoveETH(address to, uint256 amount) internal returns (address vault) {
204+
/// @solidity memory-safe-assembly
205+
assembly {
206+
to := shr(96, shl(96, to)) // Clean upper 96 bits.
207+
for { let mover := ETH_MOVER } iszero(eq(to, address())) {} {
208+
if or(lt(selfbalance(), amount), eq(to, mover)) {
209+
mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
210+
revert(0x1c, 0x04)
211+
}
212+
if extcodesize(mover) {
213+
let balanceBefore := balance(to) // Check via delta, in case `SELFDESTRUCT` is bricked.
214+
pop(call(gas(), mover, amount, codesize(), 0x00, codesize(), 0x00))
215+
if iszero(lt(add(amount, balance(to)), balanceBefore)) { break }
216+
}
217+
let m := mload(0x40)
218+
// If the mover is missing or bricked, deploy a minimal vault
219+
// that withdraws all ETH to `to` when being called only by `to`.
220+
// forgefmt: disable-next-item
221+
mstore(add(m, 0x20), 0x33146025575b600160005260206000f35b3d3d3d3d47335af1601a5760003dfd)
222+
mstore(m, or(to, shl(160, 0x6035600b3d3960353df3fe73)))
223+
// Compute and store the bytecode hash.
224+
mstore8(0x00, 0xff) // Write the prefix.
225+
mstore(0x35, keccak256(m, 0x40))
226+
mstore(0x01, shl(96, address())) // Deployer.
227+
mstore(0x15, 0) // Salt.
228+
vault := keccak256(0x00, 0x55)
229+
pop(call(gas(), vault, amount, codesize(), 0x00, codesize(), 0x00))
230+
// The vault returns a single word on success. Failure reverts with empty data.
231+
if iszero(returndatasize()) {
232+
if iszero(create2(0, m, 0x40, 0)) { revert(codesize(), codesize()) } // For gas estimation.
233+
}
234+
mstore(0x40, m) // Restore the free memory pointer.
235+
break
236+
}
237+
}
238+
}
239+
195240
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
196241
/* ERC20 OPERATIONS */
197242
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

test/SafeTransferLib.t.sol

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,4 +1176,136 @@ contract SafeTransferLibTest is SoladyTest {
11761176
function totalSupplyQuery(address token) public view returns (uint256) {
11771177
return SafeTransferLib.totalSupply(token);
11781178
}
1179+
1180+
function testSaveMoveETHViaVault(bytes32) public {
1181+
address to = _randomUniqueHashedAddress();
1182+
assertEq(to.balance, 0);
1183+
1184+
uint256 amount0 = _bound(_random(), 0, 2 ** 128 - 1);
1185+
uint256 amount1 = _bound(_random(), 0, 2 ** 128 - 1);
1186+
vm.deal(address(this), 2 ** 160 - 1);
1187+
address vault = this.safeMoveETH(to, amount0);
1188+
assertEq(vault.balance, amount0);
1189+
assertEq(this.safeMoveETH(to, amount1), vault);
1190+
assertEq(vault.balance, amount0 + amount1);
1191+
1192+
address pranker = _randomUniqueHashedAddress();
1193+
vm.prank(pranker);
1194+
(bool success,) = vault.call("");
1195+
require(success);
1196+
assertEq(vault.balance, amount0 + amount1);
1197+
assertEq(to.balance, 0);
1198+
1199+
vm.prank(to);
1200+
(success,) = vault.call("");
1201+
require(success);
1202+
assertEq(vault.balance, 0);
1203+
assertEq(to.balance, amount0 + amount1);
1204+
}
1205+
1206+
function safeMoveETHViaMover(bytes32) public {
1207+
_deployETHMover();
1208+
1209+
address to = _randomHashedAddress();
1210+
assertEq(to.balance, 0);
1211+
1212+
uint256 amount0 = _bound(_random(), 0, 2 ** 128 - 1);
1213+
uint256 amount1 = _bound(_random(), 0, 2 ** 128 - 1);
1214+
vm.deal(address(this), 2 ** 160 - 1);
1215+
uint256 selfBalanceBefore = address(this).balance;
1216+
assertEq(SafeTransferLib.safeMoveETH(to, amount0), address(0));
1217+
1218+
assertEq(to.balance, amount0);
1219+
assertEq(address(this).balance, selfBalanceBefore - amount0);
1220+
1221+
if (SafeTransferLib.ETH_MOVER.code.length == 0) {
1222+
address vault = this.safeMoveETH(to, amount0);
1223+
assertEq(vault.balance, amount1);
1224+
assertEq(to.balance, amount0);
1225+
assertEq(address(this).balance, selfBalanceBefore - amount0);
1226+
} else {
1227+
assertEq(this.safeMoveETH(to, amount0), address(0));
1228+
assertEq(to.balance, amount0 + amount1);
1229+
assertEq(address(this).balance, selfBalanceBefore - amount0 - amount1);
1230+
}
1231+
}
1232+
1233+
function testSaveMoveETHToSelfIsNoOp(bytes32) public {
1234+
if (_randomChance(2)) _deployETHMover();
1235+
address to = address(this);
1236+
uint256 amount = _bound(_random(), 0, 2 ** 128 - 1);
1237+
vm.deal(address(this), 2 ** 160 - 1);
1238+
uint256 selfBalanceBefore = address(this).balance;
1239+
assertEq(this.safeMoveETH(to, amount), address(0));
1240+
assertEq(address(this).balance, selfBalanceBefore);
1241+
}
1242+
1243+
function testSaveMoveETHToMoverReverts(bytes32) public {
1244+
if (_randomChance(2)) _deployETHMover();
1245+
address to = SafeTransferLib.ETH_MOVER;
1246+
1247+
uint256 amount = _bound(_random(), 0, 2 ** 128 - 1);
1248+
vm.deal(address(this), 2 ** 160 - 1);
1249+
1250+
vm.expectRevert(SafeTransferLib.ETHTransferFailed.selector);
1251+
this.safeMoveETH(to, amount);
1252+
}
1253+
1254+
function testSaveMoveETHInsufficientBalanceReverts(bytes32) public {
1255+
if (_randomChance(2)) _deployETHMover();
1256+
address to = _randomHashedAddress();
1257+
1258+
uint256 amount = _bound(_random(), 0, 2 ** 128 - 1);
1259+
vm.deal(address(this), 2 ** 128 - 1);
1260+
1261+
if (address(this).balance < amount) {
1262+
vm.expectRevert(SafeTransferLib.ETHTransferFailed.selector);
1263+
this.safeMoveETH(to, amount);
1264+
} else {
1265+
this.safeMoveETH(to, amount);
1266+
}
1267+
}
1268+
1269+
function safeMoveETH(address to, uint256 amount) public returns (address) {
1270+
return SafeTransferLib.safeMoveETH(_brutalized(to), amount);
1271+
}
1272+
1273+
function _deployETHMover() internal {
1274+
bytes memory initCode = hex"623d35ff3d526003601df3";
1275+
bytes32 salt = 0x000000000000000000000000000000000000000063d76c4f57ebf10084429e18;
1276+
address mover = _nicksCreate2(0, salt, initCode);
1277+
assertEq(mover.code, hex"3d35ff");
1278+
assertEq(mover, SafeTransferLib.ETH_MOVER);
1279+
}
1280+
1281+
function _deployOneTimeVault(address to, uint256 amount) internal returns (address vault) {
1282+
/// @solidity memory-safe-assembly
1283+
assembly {
1284+
to := shr(96, shl(96, to)) // Clean upper 96 bits.
1285+
for {} 1 {} {
1286+
let m := mload(0x40)
1287+
// If the mover is missing or bricked, deploy a minimal accrual contract
1288+
// that withdraws all ETH to `to` when being called only by `to`.
1289+
mstore(
1290+
add(m, 0x1f), 0x33146025575b600160005260206000f35b3d3d3d3d47335af1601a573d3dfd
1291+
)
1292+
mstore(m, or(to, shl(160, 0x6034600b3d3960343df3fe73)))
1293+
// Compute and store the bytecode hash.
1294+
mstore8(0x00, 0xff) // Write the prefix.
1295+
mstore(0x35, keccak256(m, 0x3f))
1296+
mstore(0x01, shl(96, address())) // Deployer.
1297+
mstore(0x15, 0) // Salt.
1298+
vault := keccak256(0x00, 0x55)
1299+
if iszero(
1300+
mul(
1301+
returndatasize(),
1302+
call(gas(), vault, amount, codesize(), 0x00, codesize(), 0x00)
1303+
)
1304+
) { if iszero(create2(0, m, 0x3f, 0)) { revert(codesize(), codesize()) } } // For gas estimation.
1305+
1306+
mstore(0x40, m)
1307+
break
1308+
}
1309+
}
1310+
}
11791311
}

0 commit comments

Comments
 (0)