From 19c2f2f5a5ea43e18dfff2b92b54b76815783d93 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 3 Feb 2025 19:26:37 +0100 Subject: [PATCH] SafeERC20.trySafeTransfer{,from} (#5483) --- .changeset/brown-seals-sing.md | 5 ++++ contracts/token/ERC20/utils/SafeERC20.sol | 14 +++++++++ test/token/ERC20/utils/SafeERC20.test.js | 36 +++++++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 .changeset/brown-seals-sing.md diff --git a/.changeset/brown-seals-sing.md b/.changeset/brown-seals-sing.md new file mode 100644 index 00000000000..7f4cff3dcc0 --- /dev/null +++ b/.changeset/brown-seals-sing.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`SafeERC20`: Add `trySafeTransfer` and `trySafeTransferFrom` that do not revert and return false if the transfer is not successful. diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index edac165bca5..347e2aa8b6c 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -42,6 +42,20 @@ library SafeERC20 { _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value))); } + /** + * @dev Variant of {safeTransfer} that returns a bool instead of reverting if the operation is not successful. + */ + function trySafeTransfer(IERC20 token, address to, uint256 value) internal returns (bool) { + return _callOptionalReturnBool(token, abi.encodeCall(token.transfer, (to, value))); + } + + /** + * @dev Variant of {safeTransferFrom} that returns a bool instead of reverting if the operation is not successful. + */ + function trySafeTransferFrom(IERC20 token, address from, address to, uint256 value) internal returns (bool) { + return _callOptionalReturnBool(token, abi.encodeCall(token.transferFrom, (from, to, value))); + } + /** * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value, * non-reverting calls are assumed to be successful. diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index 16b72bd6b1b..0ae94630d31 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -60,12 +60,24 @@ describe('SafeERC20', function () { .withArgs(this.token); }); + it('returns false on trySafeTransfer', async function () { + await expect(this.mock.$trySafeTransfer(this.token, this.receiver, 0n)) + .to.emit(this.mock, 'return$trySafeTransfer') + .withArgs(false); + }); + it('reverts on transferFrom', async function () { await expect(this.mock.$safeTransferFrom(this.token, this.mock, this.receiver, 0n)) .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation') .withArgs(this.token); }); + it('returns false on trySafeTransferFrom', async function () { + await expect(this.mock.$trySafeTransferFrom(this.token, this.mock, this.receiver, 0n)) + .to.emit(this.mock, 'return$trySafeTransferFrom') + .withArgs(false); + }); + it('reverts on increaseAllowance', async function () { // Call to 'token.allowance' does not return any data, resulting in a decoding error (revert without reason) await expect(this.mock.$safeIncreaseAllowance(this.token, this.spender, 0n)).to.be.revertedWithoutReason(); @@ -94,12 +106,24 @@ describe('SafeERC20', function () { .withArgs(this.token); }); + it('returns false on trySafeTransfer', async function () { + await expect(this.mock.$trySafeTransfer(this.token, this.receiver, 0n)) + .to.emit(this.mock, 'return$trySafeTransfer') + .withArgs(false); + }); + it('reverts on transferFrom', async function () { await expect(this.mock.$safeTransferFrom(this.token, this.mock, this.receiver, 0n)) .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation') .withArgs(this.token); }); + it('returns false on trySafeTransferFrom', async function () { + await expect(this.mock.$trySafeTransferFrom(this.token, this.mock, this.receiver, 0n)) + .to.emit(this.mock, 'return$trySafeTransferFrom') + .withArgs(false); + }); + it('reverts on increaseAllowance', async function () { await expect(this.mock.$safeIncreaseAllowance(this.token, this.spender, 0n)) .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation') @@ -357,11 +381,23 @@ function shouldOnlyRevertOnErrors() { .withArgs(this.mock, this.receiver, 10n); }); + it('returns true on trySafeTransfer', async function () { + await expect(this.mock.$trySafeTransfer(this.token, this.receiver, 10n)) + .to.emit(this.mock, 'return$trySafeTransfer') + .withArgs(true); + }); + it("doesn't revert on transferFrom", async function () { await expect(this.mock.$safeTransferFrom(this.token, this.owner, this.receiver, 10n)) .to.emit(this.token, 'Transfer') .withArgs(this.owner, this.receiver, 10n); }); + + it('returns true on trySafeTransferFrom', async function () { + await expect(this.mock.$trySafeTransferFrom(this.token, this.owner, this.receiver, 10n)) + .to.emit(this.mock, 'return$trySafeTransferFrom') + .withArgs(true); + }); }); describe('approvals', function () {