Skip to content

Commit 19c2f2f

Browse files
authored
SafeERC20.trySafeTransfer{,from} (#5483)
1 parent c089efa commit 19c2f2f

File tree

3 files changed

+55
-0
lines changed

3 files changed

+55
-0
lines changed

.changeset/brown-seals-sing.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`SafeERC20`: Add `trySafeTransfer` and `trySafeTransferFrom` that do not revert and return false if the transfer is not successful.

contracts/token/ERC20/utils/SafeERC20.sol

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,20 @@ library SafeERC20 {
4242
_callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));
4343
}
4444

45+
/**
46+
* @dev Variant of {safeTransfer} that returns a bool instead of reverting if the operation is not successful.
47+
*/
48+
function trySafeTransfer(IERC20 token, address to, uint256 value) internal returns (bool) {
49+
return _callOptionalReturnBool(token, abi.encodeCall(token.transfer, (to, value)));
50+
}
51+
52+
/**
53+
* @dev Variant of {safeTransferFrom} that returns a bool instead of reverting if the operation is not successful.
54+
*/
55+
function trySafeTransferFrom(IERC20 token, address from, address to, uint256 value) internal returns (bool) {
56+
return _callOptionalReturnBool(token, abi.encodeCall(token.transferFrom, (from, to, value)));
57+
}
58+
4559
/**
4660
* @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
4761
* non-reverting calls are assumed to be successful.

test/token/ERC20/utils/SafeERC20.test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,24 @@ describe('SafeERC20', function () {
6060
.withArgs(this.token);
6161
});
6262

63+
it('returns false on trySafeTransfer', async function () {
64+
await expect(this.mock.$trySafeTransfer(this.token, this.receiver, 0n))
65+
.to.emit(this.mock, 'return$trySafeTransfer')
66+
.withArgs(false);
67+
});
68+
6369
it('reverts on transferFrom', async function () {
6470
await expect(this.mock.$safeTransferFrom(this.token, this.mock, this.receiver, 0n))
6571
.to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
6672
.withArgs(this.token);
6773
});
6874

75+
it('returns false on trySafeTransferFrom', async function () {
76+
await expect(this.mock.$trySafeTransferFrom(this.token, this.mock, this.receiver, 0n))
77+
.to.emit(this.mock, 'return$trySafeTransferFrom')
78+
.withArgs(false);
79+
});
80+
6981
it('reverts on increaseAllowance', async function () {
7082
// Call to 'token.allowance' does not return any data, resulting in a decoding error (revert without reason)
7183
await expect(this.mock.$safeIncreaseAllowance(this.token, this.spender, 0n)).to.be.revertedWithoutReason();
@@ -94,12 +106,24 @@ describe('SafeERC20', function () {
94106
.withArgs(this.token);
95107
});
96108

109+
it('returns false on trySafeTransfer', async function () {
110+
await expect(this.mock.$trySafeTransfer(this.token, this.receiver, 0n))
111+
.to.emit(this.mock, 'return$trySafeTransfer')
112+
.withArgs(false);
113+
});
114+
97115
it('reverts on transferFrom', async function () {
98116
await expect(this.mock.$safeTransferFrom(this.token, this.mock, this.receiver, 0n))
99117
.to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
100118
.withArgs(this.token);
101119
});
102120

121+
it('returns false on trySafeTransferFrom', async function () {
122+
await expect(this.mock.$trySafeTransferFrom(this.token, this.mock, this.receiver, 0n))
123+
.to.emit(this.mock, 'return$trySafeTransferFrom')
124+
.withArgs(false);
125+
});
126+
103127
it('reverts on increaseAllowance', async function () {
104128
await expect(this.mock.$safeIncreaseAllowance(this.token, this.spender, 0n))
105129
.to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
@@ -357,11 +381,23 @@ function shouldOnlyRevertOnErrors() {
357381
.withArgs(this.mock, this.receiver, 10n);
358382
});
359383

384+
it('returns true on trySafeTransfer', async function () {
385+
await expect(this.mock.$trySafeTransfer(this.token, this.receiver, 10n))
386+
.to.emit(this.mock, 'return$trySafeTransfer')
387+
.withArgs(true);
388+
});
389+
360390
it("doesn't revert on transferFrom", async function () {
361391
await expect(this.mock.$safeTransferFrom(this.token, this.owner, this.receiver, 10n))
362392
.to.emit(this.token, 'Transfer')
363393
.withArgs(this.owner, this.receiver, 10n);
364394
});
395+
396+
it('returns true on trySafeTransferFrom', async function () {
397+
await expect(this.mock.$trySafeTransferFrom(this.token, this.owner, this.receiver, 10n))
398+
.to.emit(this.mock, 'return$trySafeTransferFrom')
399+
.withArgs(true);
400+
});
365401
});
366402

367403
describe('approvals', function () {

0 commit comments

Comments
 (0)