Skip to content

Commit 5480641

Browse files
Amxxernestognwcairoeth
authored
Reduce memory leakage from returndata in SafeERC20 (#5090)
Co-authored-by: ernestognw <[email protected]> Co-authored-by: cairo <[email protected]>
1 parent c3f8b76 commit 5480641

File tree

3 files changed

+33
-18
lines changed

3 files changed

+33
-18
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
### Breaking changes
44

55
- `ERC1967Utils`: Removed duplicate declaration of the `Upgraded`, `AdminChanged` and `BeaconUpgraded` events. These events are still available through the `IERC1967` interface located under the `contracts/interfaces/` directory. Minimum pragma version is now 0.8.21.
6-
- `Governor`, `GovernorCountingSimple`: The `_countVotes` virtual function now returns an `uint256` with the total votes casted. This change allows for more flexibility for partial and fractional voting. Upgrading users may get a compilation error that can be fixed by adding a return statement to the `_countVotes` function.
6+
- `Governor`, `GovernorCountingSimple`: The `_countVotes` virtual function now returns an `uint256` with the total votes casted. This change allows for more flexibility for partial and fractional voting. Upgrading users may get a compilation error that can be fixed by adding a return statement to the `_countVotes` function.
77

88
### Custom error changes
99

@@ -14,6 +14,9 @@ This version comes with changes to the custom error identifiers. Contracts previ
1414
- Replace `Clones.Create2InsufficientBalance` with `Errors.InsufficientBalance`
1515
- Replace `Clones.ERC1167FailedCreateClone` with `Errors.FailedDeployment`
1616
- Replace `Clones.Create2FailedDeployment` with `Errors.FailedDeployment`
17+
- `SafeERC20`: Replace `Address.AddressEmptyCode` with `SafeERC20FailedOperation` if there is no code at the token's address.
18+
- `SafeERC20`: Replace generic `Error(string)` with `SafeERC20FailedOperation` if the returned data can't be decoded as `bool`.
19+
- `SafeERC20`: Replace generic `SafeERC20FailedOperation` with the revert message from the contract call if it fails.
1720

1821
## 5.0.2 (2024-02-29)
1922

contracts/token/ERC20/utils/SafeERC20.sol

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ import {Address} from "../../../utils/Address.sol";
1717
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
1818
*/
1919
library SafeERC20 {
20-
using Address for address;
21-
2220
/**
2321
* @dev An operation with an ERC-20 token failed.
2422
*/
@@ -142,14 +140,25 @@ library SafeERC20 {
142140
* on the return value: the return value is optional (but if data is returned, it must not be false).
143141
* @param token The token targeted by the call.
144142
* @param data The call data (encoded using abi.encode or one of its variants).
143+
*
144+
* This is a variant of {_callOptionalReturnBool} that reverts if call fails to meet the requirements.
145145
*/
146146
function _callOptionalReturn(IERC20 token, bytes memory data) private {
147-
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
148-
// we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that
149-
// the target address contains contract code and also asserts for success in the low-level call.
147+
uint256 returnSize;
148+
uint256 returnValue;
149+
assembly ("memory-safe") {
150+
let success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
151+
// bubble errors
152+
if iszero(success) {
153+
let ptr := mload(0x40)
154+
returndatacopy(ptr, 0, returndatasize())
155+
revert(ptr, returndatasize())
156+
}
157+
returnSize := returndatasize()
158+
returnValue := mload(0)
159+
}
150160

151-
bytes memory returndata = address(token).functionCall(data);
152-
if (returndata.length != 0 && !abi.decode(returndata, (bool))) {
161+
if (returnSize == 0 ? address(token).code.length == 0 : returnValue != 1) {
153162
revert SafeERC20FailedOperation(address(token));
154163
}
155164
}
@@ -160,14 +169,17 @@ library SafeERC20 {
160169
* @param token The token targeted by the call.
161170
* @param data The call data (encoded using abi.encode or one of its variants).
162171
*
163-
* This is a variant of {_callOptionalReturn} that silents catches all reverts and returns a bool instead.
172+
* This is a variant of {_callOptionalReturn} that silently catches all reverts and returns a bool instead.
164173
*/
165174
function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
166-
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
167-
// we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false
168-
// and not revert is the subcall reverts.
169-
170-
(bool success, bytes memory returndata) = address(token).call(data);
171-
return success && (returndata.length == 0 || abi.decode(returndata, (bool))) && address(token).code.length > 0;
175+
bool success;
176+
uint256 returnSize;
177+
uint256 returnValue;
178+
assembly ("memory-safe") {
179+
success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
180+
returnSize := returndatasize()
181+
returnValue := mload(0)
182+
}
183+
return success && (returnSize == 0 ? address(token).code.length > 0 : returnValue == 1);
172184
}
173185
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,13 @@ describe('SafeERC20', function () {
5656

5757
it('reverts on transfer', async function () {
5858
await expect(this.mock.$safeTransfer(this.token, this.receiver, 0n))
59-
.to.be.revertedWithCustomError(this.mock, 'AddressEmptyCode')
59+
.to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
6060
.withArgs(this.token);
6161
});
6262

6363
it('reverts on transferFrom', async function () {
6464
await expect(this.mock.$safeTransferFrom(this.token, this.mock, this.receiver, 0n))
65-
.to.be.revertedWithCustomError(this.mock, 'AddressEmptyCode')
65+
.to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
6666
.withArgs(this.token);
6767
});
6868

@@ -78,7 +78,7 @@ describe('SafeERC20', function () {
7878

7979
it('reverts on forceApprove', async function () {
8080
await expect(this.mock.$forceApprove(this.token, this.spender, 0n))
81-
.to.be.revertedWithCustomError(this.mock, 'AddressEmptyCode')
81+
.to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
8282
.withArgs(this.token);
8383
});
8484
});

0 commit comments

Comments
 (0)