diff --git a/foundry.toml b/foundry.toml index 596d69b..db742ed 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,6 +7,7 @@ ffi = true # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options remappings = [ + 'contract-libs/=src/', 'forge-std/=lib/forge-std/src/', 'ds-test/=lib/forge-std/lib/ds-test/src/', '@openzeppelin/=lib/openzeppelin-contracts/', diff --git a/src/transfers/LibNativeTransfer.sol b/src/transfers/LibNativeTransfer.sol new file mode 100644 index 0000000..0a0dee3 --- /dev/null +++ b/src/transfers/LibNativeTransfer.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { LibErrorHandler } from "../LibErrorHandler.sol"; + +/** + * @title NativeTransferHelper + */ +library LibNativeTransfer { + using LibErrorHandler for bool; + + /** + * @dev Transfers Native Coin and wraps result for the method caller to a recipient. + */ + function transfer(address to, uint256 value, uint256 gasAmount) internal { + (bool success, bytes memory returnOrRevertData) = trySendValue(to, value, gasAmount); + success.handleRevert(bytes4(0x0), returnOrRevertData); + } + + /** + * @dev Unsafe send `amount` Native to the address `to`. If the sender's balance is insufficient, + * the call does not revert. + * + * Note: + * - Does not assert whether the balance of sender is sufficient. + * - Does not assert whether the recipient accepts NATIVE. + * - Consider using `ReentrancyGuard` before calling this function. + * + */ + function trySendValue(address to, uint256 value, uint256 gasAmount) + internal + returns (bool success, bytes memory returnOrRevertData) + { + (success, returnOrRevertData) = to.call{ value: value, gas: gasAmount }(""); + } +} diff --git a/test/transfers/LibNativeTransfer.t.sol b/test/transfers/LibNativeTransfer.t.sol new file mode 100644 index 0000000..0a24d69 --- /dev/null +++ b/test/transfers/LibNativeTransfer.t.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { Test } from "forge-std/Test.sol"; +import { LibNativeTransfer } from "contract-libs/transfers/LibNativeTransfer.sol"; + +contract LibNativeTransferTest is Test { + function testFork_RevertWhen_TransferNativeToContractWithoutFallback_safeTransfer( + address any, + uint256 amount, + uint256 gas + ) external { + vm.deal(any, amount); + vm.expectRevert(); + vm.prank(any); + LibNativeTransfer.transfer(address(this), amount, gas); + } + + function testConcrete_TransferNative(uint256 gas) external { + LibNativeTransfer.transfer(address(0xBEEF), 1e18, gas); + assertEq(address(0xBEEF).balance, 1e18); + } + + function testFork_TransferNativeToRecipient(address recipient, uint256 amount, uint256 gas) external { + // Transferring to msg.sender can fail because it's possible to overflow their ETH balance as it begins non-zero. + if (recipient.code.length > 0 || uint256(uint160(recipient)) <= 18 || recipient == msg.sender) return; + + amount = bound(amount, 0, address(this).balance); + LibNativeTransfer.transfer(recipient, amount, gas); + + assertEq(recipient.balance, amount); + } +}