diff --git a/contracts/libraries/SafeSendLib.sol b/contracts/libraries/SafeSendLib.sol new file mode 100644 index 00000000..19eef5a4 --- /dev/null +++ b/contracts/libraries/SafeSendLib.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @title SafeSendLib + * @dev Library for safely sending Ether to a recipient address. + */ +library SafeSendLib { + /** + * @dev Sends `amount` wei to address `to` in constructor by calling selfdestruct. + * Even recipients that are contracts with revert in fallback() or receive() will receive the funds. + * @param to The address to send Ether to. + * @param amount The amount of wei to send. + */ + function safeSend(address to, uint256 amount) internal { + assembly ("memory-safe") { // solhint-disable-line no-inline-assembly + mstore(0, to) + mstore8(11, 0x73) // 0x73 = PUSH20 opcode + mstore8(32, 0xff) // 0xff = SELFDESTRUCT opcode + pop(create(amount, 11, 22)) + } + } +} diff --git a/contracts/libraries/UniERC20.sol b/contracts/libraries/UniERC20.sol index d7a02627..eba95f23 100644 --- a/contracts/libraries/UniERC20.sol +++ b/contracts/libraries/UniERC20.sol @@ -7,6 +7,7 @@ import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../interfaces/IERC20MetadataUppercase.sol"; import "./SafeERC20.sol"; import "./StringUtil.sol"; +import "./SafeSendLib.sol"; /** * @title UniERC20 @@ -15,6 +16,7 @@ import "./StringUtil.sol"; */ library UniERC20 { using SafeERC20 for IERC20; + using SafeSendLib for address payable; error InsufficientBalance(); error ApproveCalledOnETH(); @@ -73,6 +75,29 @@ library UniERC20 { } } + /** + * @dev Transfers a specified amount of the token to a given address. + * Note: Does nothing if the amount is zero. + * Note 2: Uses the SafeSendLib for safe Ether transfers. + * @param token The token to transfer. + * @param to The address to transfer the token to. + * @param amount The amount of the token to transfer. + */ + function uniSafeTransfer( + IERC20 token, + address payable to, + uint256 amount + ) internal { + if (amount > 0) { + if (isETH(token)) { + if (address(this).balance < amount) revert InsufficientBalance(); + to.safeSend(amount); + } else { + token.safeTransfer(to, amount); + } + } + } + /** * @dev Transfers a specified amount of the token from one address to another. * Note: Does nothing if the amount is zero. @@ -106,6 +131,38 @@ library UniERC20 { } } + /** + * @dev Transfers a specified amount of the token from one address to another. + * Note: Does nothing if the amount is zero. + * Note 2: Uses the SafeSendLib for safe Ether transfers. + * @param token The token to transfer. + * @param from The address to transfer the token from. + * @param to The address to transfer the token to. + * @param amount The amount of the token to transfer. + */ + function uniSafeTransferFrom( + IERC20 token, + address payable from, + address to, + uint256 amount + ) internal { + if (amount > 0) { + if (isETH(token)) { + if (msg.value < amount) revert NotEnoughValue(); + if (from != msg.sender) revert FromIsNotSender(); + if (to != address(this)) revert ToIsNotThis(); + if (msg.value > amount) { + // Return remainder if exist + unchecked { + from.safeSend(msg.value - amount); + } + } + } else { + token.safeTransferFrom(from, to, amount); + } + } + } + /** * @dev Retrieves the symbol from ERC20 metadata of the specified token. * @param token The token to retrieve the symbol of. diff --git a/contracts/tests/mocks/UniERC20Helper.sol b/contracts/tests/mocks/UniERC20Helper.sol index 42b05b77..e61f055b 100644 --- a/contracts/tests/mocks/UniERC20Helper.sol +++ b/contracts/tests/mocks/UniERC20Helper.sol @@ -12,6 +12,12 @@ interface IUniERC20Wrapper { address to, uint256 amount ) external payable; + + function safeTransferFrom( + address payable from, + address to, + uint256 amount + ) external payable; } contract UniERC20Wrapper { @@ -27,6 +33,10 @@ contract UniERC20Wrapper { _token.uniTransfer(to, amount); } + function safeTransfer(address payable to, uint256 amount) external payable { + _token.uniSafeTransfer(to, amount); + } + function transferFrom( address payable from, address to, @@ -35,6 +45,14 @@ contract UniERC20Wrapper { _token.uniTransferFrom(from, to, amount); } + function safeTransferFrom( + address payable from, + address to, + uint256 amount + ) external payable { + _token.uniSafeTransferFrom(from, to, amount); + } + function approve(address spender, uint256 amount) external { _token.uniApprove(spender, amount); } @@ -73,6 +91,10 @@ contract ETHBadReceiver { _wrapper.transferFrom{value: msg.value}(payable(address(this)), to, amount); } + function safeTransfer(address to, uint256 amount) external payable { + _wrapper.safeTransferFrom{value: msg.value}(payable(address(this)), to, amount); + } + receive() external payable { revert ReceiveFailed(); } diff --git a/test/contracts/UniERC20.test.ts b/test/contracts/UniERC20.test.ts index 75ccf094..920a3155 100644 --- a/test/contracts/UniERC20.test.ts +++ b/test/contracts/UniERC20.test.ts @@ -38,6 +38,13 @@ describe('UniERC20', function () { expect(await wrapper.balanceOf(signer2)).to.be.equal(100); }); + it('uni safe transfer', async function () { + const { wrapper, token } = await loadFixture(deployMocks); + await token.transfer(wrapper, 100); + await wrapper.safeTransfer(signer2, 100); + expect(await wrapper.balanceOf(signer2)).to.be.equal(100); + }); + it('uni transfer from', async function () { const { wrapper, token } = await loadFixture(deployMocks); await token.transfer(signer2, 100); @@ -46,6 +53,14 @@ describe('UniERC20', function () { expect(await wrapper.balanceOf(signer3)).to.be.equal(100); }); + it('uni safe transfer from', async function () { + const { wrapper, token } = await loadFixture(deployMocks); + await token.transfer(signer2, 100); + await token.connect(signer2).approve(wrapper, 100); + await wrapper.safeTransferFrom(signer2, signer3, 100); + expect(await wrapper.balanceOf(signer3)).to.be.equal(100); + }); + it('uni approve', async function () { const { wrapper, token } = await loadFixture(deployMocks); await token.transfer(wrapper, 100); @@ -248,6 +263,22 @@ describe('UniERC20', function () { }), ).to.eventually.be.rejectedWith('ETHTransferFailed'); }); + + it('uni safe transfer, success', async function () { + const { wrapper, receiver } = await loadFixture(deployMocks); + const balBefore = await wrapper.balanceOf(receiver); + await wrapper.safeTransfer(receiver, 100, { value: 100 }); + const balAfter = await wrapper.balanceOf(receiver); + expect(balAfter - balBefore).to.be.equal(100); + }); + + it('uni safe transferFrom, success', async function () { + const { wrapper, receiver } = await loadFixture(deployMocks); + const balBefore = await wrapper.balanceOf(wrapper); + await receiver.safeTransfer(wrapper, 100, { value: 101n }); + const balAfter = await wrapper.balanceOf(wrapper); + expect(balAfter - balBefore).to.be.equal(100); + }); }); describe('ETH from special address', function () {