diff --git a/src/utils/SafeTransfer.sol b/src/utils/SafeTransfer.sol new file mode 100644 index 0000000..076b342 --- /dev/null +++ b/src/utils/SafeTransfer.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +/// @notice Safe ETH and ERC20 free function transfer collection that gracefully handles missing return values. +/// @author SolDAO (https://github.com/Sol-DAO/solbase/blob/main/src/utils/SafeTransfer.sol) +/// @author Modified from Zolidity (https://github.com/z0r0z/zolidity/blob/main/src/utils/SafeTransfer.sol) + +/// @dev The ETH transfer has failed. +error ETHTransferFailed(); + +/// @dev Sends `amount` (in wei) ETH to `to`. +/// Reverts upon failure. +function safeTransferETH(address to, uint256 amount) { + assembly { + // Transfer the ETH and check if it succeeded or not. + if iszero(call(gas(), to, amount, 0, 0, 0, 0)) { + // Store the function selector of `ETHTransferFailed()`. + mstore(0x00, 0xb12d13eb) + // Revert with (offset, size). + revert(0x1c, 0x04) + } + } +} + +/// @dev The ERC20 `approve` has failed. +error ApproveFailed(); + +/// @dev Sets `amount` of ERC20 `token` for `to` to manage on behalf of the current contract. +/// Reverts upon failure. +function safeApprove( + address token, + address to, + uint256 amount +) { + assembly { + // We'll write our calldata to this slot below, but restore it later. + let memPointer := mload(0x40) + + // Write the abi-encoded calldata into memory, beginning with the function selector. + mstore(0x00, 0x095ea7b3) + mstore(0x20, to) // Append the "to" argument. + mstore(0x40, amount) // Append the "amount" argument. + + if iszero( + and( + // Set success to whether the call reverted, if not we check it either + // returned exactly 1 (can't just be non-zero data), or had no return data. + or(eq(mload(0x00), 1), iszero(returndatasize())), + // We use 0x44 because that's the total length of our calldata (0x04 + 0x20 * 2) + // Counterintuitively, this call() must be positioned after the or() in the + // surrounding and() because and() evaluates its arguments from right to left. + call(gas(), token, 0, 0x1c, 0x44, 0x00, 0x20) + ) + ) { + // Store the function selector of `ApproveFailed()`. + mstore(0x00, 0x3e3f8f73) + // Revert with (offset, size). + revert(0x1c, 0x04) + } + + mstore(0x40, memPointer) // Restore the memPointer. + } +} + +/// @dev The ERC20 `transfer` has failed. +error TransferFailed(); + +/// @dev Sends `amount` of ERC20 `token` from the current contract to `to`. +/// Reverts upon failure. +function safeTransfer( + address token, + address to, + uint256 amount +) { + assembly { + // We'll write our calldata to this slot below, but restore it later. + let memPointer := mload(0x40) + + // Write the abi-encoded calldata into memory, beginning with the function selector. + mstore(0x00, 0xa9059cbb) + mstore(0x20, to) // Append the "to" argument. + mstore(0x40, amount) // Append the "amount" argument. + + if iszero( + and( + // Set success to whether the call reverted, if not we check it either + // returned exactly 1 (can't just be non-zero data), or had no return data. + or(eq(mload(0x00), 1), iszero(returndatasize())), + // We use 0x44 because that's the total length of our calldata (0x04 + 0x20 * 2) + // Counterintuitively, this call() must be positioned after the or() in the + // surrounding and() because and() evaluates its arguments from right to left. + call(gas(), token, 0, 0x1c, 0x44, 0x00, 0x20) + ) + ) { + // Store the function selector of `TransferFailed()`. + mstore(0x00, 0x90b8ec18) + // Revert with (offset, size). + revert(0x1c, 0x04) + } + + mstore(0x40, memPointer) // Restore the memPointer. + } +} + +/// @dev The ERC20 `transferFrom` has failed. +error TransferFromFailed(); + +/// @dev Sends `amount` of ERC20 `token` from `from` to `to`. +/// Reverts upon failure. +/// +/// The `from` account must have at least `amount` approved for +/// the current contract to manage. +function safeTransferFrom( + address token, + address from, + address to, + uint256 amount +) { + assembly { + // We'll write our calldata to this slot below, but restore it later. + let memPointer := mload(0x40) + + // Write the abi-encoded calldata into memory, beginning with the function selector. + mstore(0x00, 0x23b872dd) + mstore(0x20, from) // Append the "from" argument. + mstore(0x40, to) // Append the "to" argument. + mstore(0x60, amount) // Append the "amount" argument. + + if iszero( + and( + // Set success to whether the call reverted, if not we check it either + // returned exactly 1 (can't just be non-zero data), or had no return data. + or(eq(mload(0x00), 1), iszero(returndatasize())), + // We use 0x64 because that's the total length of our calldata (0x04 + 0x20 * 3) + // Counterintuitively, this call() must be positioned after the or() in the + // surrounding and() because and() evaluates its arguments from right to left. + call(gas(), token, 0, 0x1c, 0x64, 0x00, 0x20) + ) + ) { + // Store the function selector of `TransferFromFailed()`. + mstore(0x00, 0x7939f424) + // Revert with (offset, size). + revert(0x1c, 0x04) + } + + mstore(0x60, 0) // Restore the zero slot to zero. + mstore(0x40, memPointer) // Restore the memPointer. + } +}