diff --git a/audit/auditLog.json b/audit/auditLog.json index 3127e5784..6b57417ce 100644 --- a/audit/auditLog.json +++ b/audit/auditLog.json @@ -13,6 +13,13 @@ "auditorGitHandle": "sujithsomraaj", "auditReportPath": "./audit/reports/2024.09.13_EmergencyPauseFacet.pdf", "auditCommitHash": "77441a088e0789513db4e068f7ef6c5c0988ee42" + }, + "audit20241014": { + "auditCompletedOn": "14.10.2024", + "auditedBy": "Sujith Somraaj (individual security researcher)", + "auditorGitHandle": "sujithsomraaj", + "auditReportPath": "./audit/reports/2024.10.14_WithdrawablePeriphery.pdf", + "auditCommitHash": "b03e658b359f1696388e2aaeaf24a7c107ba462a" } }, "auditedContracts": { @@ -21,6 +28,9 @@ }, "StargateFacetV2": { "1.0.1": ["audit20240814"] + }, + "WithdrawablePeriphery": { + "1.0.0": ["audit20241014"] } } } diff --git a/audit/reports/2024.10.14_WithdrawablePeriphery.pdf b/audit/reports/2024.10.14_WithdrawablePeriphery.pdf new file mode 100644 index 000000000..90125aa5a Binary files /dev/null and b/audit/reports/2024.10.14_WithdrawablePeriphery.pdf differ diff --git a/src/Helpers/WithdrawablePeriphery.sol b/src/Helpers/WithdrawablePeriphery.sol new file mode 100644 index 000000000..9e4a08203 --- /dev/null +++ b/src/Helpers/WithdrawablePeriphery.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +/// @custom:version 1.0.0 +pragma solidity 0.8.17; + +import { TransferrableOwnership } from "./TransferrableOwnership.sol"; +import { LibAsset } from "../Libraries/LibAsset.sol"; +import { ExternalCallFailed } from "../Errors/GenericErrors.sol"; +import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; + +abstract contract WithdrawablePeriphery is TransferrableOwnership { + using SafeTransferLib for address; + + event TokensWithdrawn( + address assetId, + address payable receiver, + uint256 amount + ); + + constructor(address _owner) TransferrableOwnership(_owner) {} + + function withdrawToken( + address assetId, + address payable receiver, + uint256 amount + ) external onlyOwner { + if (LibAsset.isNativeAsset(assetId)) { + // solhint-disable-next-line avoid-low-level-calls + (bool success, ) = receiver.call{ value: amount }(""); + if (!success) revert ExternalCallFailed(); + } else { + assetId.safeTransfer(receiver, amount); + } + + emit TokensWithdrawn(assetId, receiver, amount); + } +} diff --git a/test/solidity/Helpers/WithdrawablePeriphery.t.sol b/test/solidity/Helpers/WithdrawablePeriphery.t.sol new file mode 100644 index 000000000..bba1a0f12 --- /dev/null +++ b/test/solidity/Helpers/WithdrawablePeriphery.t.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +/// @custom:version 1.0.0 +pragma solidity 0.8.17; + +import { WithdrawablePeriphery } from "lifi/Helpers/WithdrawablePeriphery.sol"; + +import { TestBase } from "../utils/TestBase.sol"; +import { NonETHReceiver } from "../utils/TestHelpers.sol"; + +contract TestContract is WithdrawablePeriphery { + constructor(address _owner) WithdrawablePeriphery(_owner) {} +} + +contract WithdrawablePeripheryTest is TestBase { + WithdrawablePeriphery internal withdrawable; + + event TokensWithdrawn( + address assetId, + address payable receiver, + uint256 amount + ); + + error UnAuthorized(); + + function setUp() public { + initTestBase(); + + // deploy contract + withdrawable = new TestContract(USER_DIAMOND_OWNER); + + // fund contract with native and ERC20 + deal( + ADDRESS_USDC, + address(withdrawable), + 100_000 * 10 ** usdc.decimals() + ); + deal(address(withdrawable), 1 ether); + } + + function test_AllowsOwnerToWithdrawNative() public { + uint256 withdrawAmount = 0.1 ether; + + vm.startPrank(USER_DIAMOND_OWNER); + + vm.expectEmit(true, true, true, true, address(withdrawable)); + emit TokensWithdrawn( + address(0), + payable(USER_RECEIVER), + withdrawAmount + ); + + withdrawable.withdrawToken( + address(0), + payable(USER_RECEIVER), + withdrawAmount + ); + } + + function test_AllowsOwnerToWithdrawERC20() public { + uint256 withdrawAmount = 10 * 10 ** usdc.decimals(); + vm.startPrank(USER_DIAMOND_OWNER); + + vm.expectEmit(true, true, true, true, address(withdrawable)); + emit TokensWithdrawn( + ADDRESS_USDC, + payable(USER_RECEIVER), + withdrawAmount + ); + + withdrawable.withdrawToken( + ADDRESS_USDC, + payable(USER_RECEIVER), + withdrawAmount + ); + } + + function testRevert_FailsIfNonOwnerTriesToWithdrawNative() public { + uint256 withdrawAmount = 0.1 ether; + + vm.startPrank(USER_SENDER); + + vm.expectRevert(UnAuthorized.selector); + + withdrawable.withdrawToken( + address(0), + payable(USER_RECEIVER), + withdrawAmount + ); + } + + function testRevert_FailsIfNonOwnerTriesToWithdrawERC20() public { + uint256 withdrawAmount = 10 * 10 ** usdc.decimals(); + vm.startPrank(USER_SENDER); + + vm.expectRevert(UnAuthorized.selector); + + withdrawable.withdrawToken( + ADDRESS_USDC, + payable(USER_RECEIVER), + withdrawAmount + ); + } + + function testRevert_FailsIfNativeTokenTransferFails() public { + uint256 withdrawAmount = 0.1 ether; + + address nonETHReceiver = address(new NonETHReceiver()); + + vm.startPrank(USER_DIAMOND_OWNER); + + vm.expectRevert(); + + withdrawable.withdrawToken( + address(0), + payable(nonETHReceiver), + withdrawAmount + ); + } +}