Add ERC1363 implementation (#4631)
Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com> Co-authored-by: ernestognw <ernestognw@gmail.com>
This commit is contained in:
committed by
GitHub
parent
a51f1e1354
commit
e5f02bc608
5
.changeset/friendly-nails-push.md
Normal file
5
.changeset/friendly-nails-push.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
'openzeppelin-solidity': minor
|
||||
---
|
||||
|
||||
`ERC1363`: Add implementation of the token payable standard allowing execution of contract code after transfers and approvals.
|
||||
5
.changeset/nice-paws-pull.md
Normal file
5
.changeset/nice-paws-pull.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
'openzeppelin-solidity': minor
|
||||
---
|
||||
|
||||
`SafeERC20`: Add "relaxed" function for interacting with ERC-1363 functions in a way that is compatible with EOAs.
|
||||
@ -7,13 +7,13 @@ import {IERC20} from "./IERC20.sol";
|
||||
import {IERC165} from "./IERC165.sol";
|
||||
|
||||
/**
|
||||
* @dev Interface of an ERC-1363 compliant contract, as defined in the
|
||||
* https://eips.ethereum.org/EIPS/eip-1363[ERC].
|
||||
* @title IERC1363
|
||||
* @dev Interface of the ERC-1363 standard as defined in the https://eips.ethereum.org/EIPS/eip-1363[ERC-1363].
|
||||
*
|
||||
* Defines a interface for ERC-20 tokens that supports executing recipient
|
||||
* code after `transfer` or `transferFrom`, or spender code after `approve`.
|
||||
* Defines an extension interface for ERC-20 tokens that supports executing code on a recipient contract
|
||||
* after `transfer` or `transferFrom`, or code on a spender contract after `approve`, in a single transaction.
|
||||
*/
|
||||
interface IERC1363 is IERC165, IERC20 {
|
||||
interface IERC1363 is IERC20, IERC165 {
|
||||
/*
|
||||
* Note: the ERC-165 identifier for this interface is 0xb0202a11.
|
||||
* 0xb0202a11 ===
|
||||
@ -26,55 +26,61 @@ interface IERC1363 is IERC165, IERC20 {
|
||||
*/
|
||||
|
||||
/**
|
||||
* @dev Transfer tokens from `msg.sender` to another address and then call `onTransferReceived` on receiver
|
||||
* @param to address The address which you want to transfer to
|
||||
* @param amount uint256 The amount of tokens to be transferred
|
||||
* @return true unless throwing
|
||||
* @dev Moves a `value` amount of tokens from the caller's account to `to`
|
||||
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
|
||||
* @param to The address which you want to transfer to.
|
||||
* @param value The amount of tokens to be transferred.
|
||||
* @return A boolean value indicating whether the operation succeeded unless throwing.
|
||||
*/
|
||||
function transferAndCall(address to, uint256 amount) external returns (bool);
|
||||
function transferAndCall(address to, uint256 value) external returns (bool);
|
||||
|
||||
/**
|
||||
* @dev Transfer tokens from `msg.sender` to another address and then call `onTransferReceived` on receiver
|
||||
* @param to address The address which you want to transfer to
|
||||
* @param amount uint256 The amount of tokens to be transferred
|
||||
* @param data bytes Additional data with no specified format, sent in call to `to`
|
||||
* @return true unless throwing
|
||||
* @dev Moves a `value` amount of tokens from the caller's account to `to`
|
||||
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
|
||||
* @param to The address which you want to transfer to.
|
||||
* @param value The amount of tokens to be transferred.
|
||||
* @param data Additional data with no specified format, sent in call to `to`.
|
||||
* @return A boolean value indicating whether the operation succeeded unless throwing.
|
||||
*/
|
||||
function transferAndCall(address to, uint256 amount, bytes memory data) external returns (bool);
|
||||
function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool);
|
||||
|
||||
/**
|
||||
* @dev Transfer tokens from one address to another and then call `onTransferReceived` on receiver
|
||||
* @param from address The address which you want to send tokens from
|
||||
* @param to address The address which you want to transfer to
|
||||
* @param amount uint256 The amount of tokens to be transferred
|
||||
* @return true unless throwing
|
||||
* @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
|
||||
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
|
||||
* @param from The address which you want to send tokens from.
|
||||
* @param to The address which you want to transfer to.
|
||||
* @param value The amount of tokens to be transferred.
|
||||
* @return A boolean value indicating whether the operation succeeded unless throwing.
|
||||
*/
|
||||
function transferFromAndCall(address from, address to, uint256 amount) external returns (bool);
|
||||
function transferFromAndCall(address from, address to, uint256 value) external returns (bool);
|
||||
|
||||
/**
|
||||
* @dev Transfer tokens from one address to another and then call `onTransferReceived` on receiver
|
||||
* @param from address The address which you want to send tokens from
|
||||
* @param to address The address which you want to transfer to
|
||||
* @param amount uint256 The amount of tokens to be transferred
|
||||
* @param data bytes Additional data with no specified format, sent in call to `to`
|
||||
* @return true unless throwing
|
||||
* @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
|
||||
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
|
||||
* @param from The address which you want to send tokens from.
|
||||
* @param to The address which you want to transfer to.
|
||||
* @param value The amount of tokens to be transferred.
|
||||
* @param data Additional data with no specified format, sent in call to `to`.
|
||||
* @return A boolean value indicating whether the operation succeeded unless throwing.
|
||||
*/
|
||||
function transferFromAndCall(address from, address to, uint256 amount, bytes memory data) external returns (bool);
|
||||
function transferFromAndCall(address from, address to, uint256 value, bytes calldata data) external returns (bool);
|
||||
|
||||
/**
|
||||
* @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender
|
||||
* and then call `onApprovalReceived` on spender.
|
||||
* @param spender address The address which will spend the funds
|
||||
* @param amount uint256 The amount of tokens to be spent
|
||||
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
|
||||
* caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
|
||||
* @param spender The address which will spend the funds.
|
||||
* @param value The amount of tokens to be spent.
|
||||
* @return A boolean value indicating whether the operation succeeded unless throwing.
|
||||
*/
|
||||
function approveAndCall(address spender, uint256 amount) external returns (bool);
|
||||
function approveAndCall(address spender, uint256 value) external returns (bool);
|
||||
|
||||
/**
|
||||
* @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender
|
||||
* and then call `onApprovalReceived` on spender.
|
||||
* @param spender address The address which will spend the funds
|
||||
* @param amount uint256 The amount of tokens to be spent
|
||||
* @param data bytes Additional data with no specified format, sent in call to `spender`
|
||||
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
|
||||
* caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
|
||||
* @param spender The address which will spend the funds.
|
||||
* @param value The amount of tokens to be spent.
|
||||
* @param data Additional data with no specified format, sent in call to `spender`.
|
||||
* @return A boolean value indicating whether the operation succeeded unless throwing.
|
||||
*/
|
||||
function approveAndCall(address spender, uint256 amount, bytes memory data) external returns (bool);
|
||||
function approveAndCall(address spender, uint256 value, bytes calldata data) external returns (bool);
|
||||
}
|
||||
|
||||
@ -4,32 +4,29 @@
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
/**
|
||||
* @dev Interface for any contract that wants to support {IERC1363-transferAndCall}
|
||||
* or {IERC1363-transferFromAndCall} from {ERC1363} token contracts.
|
||||
* @title IERC1363Receiver
|
||||
* @dev Interface for any contract that wants to support `transferAndCall` or `transferFromAndCall`
|
||||
* from ERC-1363 token contracts.
|
||||
*/
|
||||
interface IERC1363Receiver {
|
||||
/*
|
||||
* Note: the ERC-165 identifier for this interface is 0x88a7ca5c.
|
||||
* 0x88a7ca5c === bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"))
|
||||
*/
|
||||
|
||||
/**
|
||||
* @notice Handle the receipt of ERC-1363 tokens
|
||||
* @dev Any ERC-1363 smart contract calls this function on the recipient
|
||||
* after a `transfer` or a `transferFrom`. This function MAY throw to revert and reject the
|
||||
* transfer. Return of other than the magic value MUST result in the
|
||||
* transaction being reverted.
|
||||
* Note: the token contract address is always the message sender.
|
||||
* @param operator address The address which called `transferAndCall` or `transferFromAndCall` function
|
||||
* @param from address The address which are token transferred from
|
||||
* @param amount uint256 The amount of tokens transferred
|
||||
* @param data bytes Additional data with no specified format
|
||||
* @return `bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"))` unless throwing
|
||||
* @dev Whenever ERC-1363 tokens are transferred to this contract via `transferAndCall` or `transferFromAndCall`
|
||||
* by `operator` from `from`, this function is called.
|
||||
*
|
||||
* NOTE: To accept the transfer, this must return
|
||||
* `bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"))`
|
||||
* (i.e. 0x88a7ca5c, or its own function selector).
|
||||
*
|
||||
* @param operator The address which called `transferAndCall` or `transferFromAndCall` function.
|
||||
* @param from The address which are tokens transferred from.
|
||||
* @param value The amount of tokens transferred.
|
||||
* @param data Additional data with no specified format.
|
||||
* @return `bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"))` if transfer is allowed unless throwing.
|
||||
*/
|
||||
function onTransferReceived(
|
||||
address operator,
|
||||
address from,
|
||||
uint256 amount,
|
||||
bytes memory data
|
||||
uint256 value,
|
||||
bytes calldata data
|
||||
) external returns (bytes4);
|
||||
}
|
||||
|
||||
@ -4,26 +4,23 @@
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
/**
|
||||
* @dev Interface for any contract that wants to support {IERC1363-approveAndCall}
|
||||
* from {ERC1363} token contracts.
|
||||
* @title ERC1363Spender
|
||||
* @dev Interface for any contract that wants to support `approveAndCall`
|
||||
* from ERC-1363 token contracts.
|
||||
*/
|
||||
interface IERC1363Spender {
|
||||
/*
|
||||
* Note: the ERC-165 identifier for this interface is 0x7b04a2d0.
|
||||
* 0x7b04a2d0 === bytes4(keccak256("onApprovalReceived(address,uint256,bytes)"))
|
||||
*/
|
||||
|
||||
/**
|
||||
* @notice Handle the approval of ERC-1363 tokens
|
||||
* @dev Any ERC-1363 smart contract calls this function on the recipient
|
||||
* after an `approve`. This function MAY throw to revert and reject the
|
||||
* approval. Return of other than the magic value MUST result in the
|
||||
* transaction being reverted.
|
||||
* Note: the token contract address is always the message sender.
|
||||
* @param owner address The address which called `approveAndCall` function
|
||||
* @param amount uint256 The amount of tokens to be spent
|
||||
* @param data bytes Additional data with no specified format
|
||||
* @return `bytes4(keccak256("onApprovalReceived(address,uint256,bytes)"))`unless throwing
|
||||
* @dev Whenever an ERC-1363 token `owner` approves this contract via `approveAndCall`
|
||||
* to spend their tokens, this function is called.
|
||||
*
|
||||
* NOTE: To accept the approval, this must return
|
||||
* `bytes4(keccak256("onApprovalReceived(address,uint256,bytes)"))`
|
||||
* (i.e. 0x7b04a2d0, or its own function selector).
|
||||
*
|
||||
* @param owner The address which called `approveAndCall` function and previously owned the tokens.
|
||||
* @param value The amount of tokens to be spent.
|
||||
* @param data Additional data with no specified format.
|
||||
* @return `bytes4(keccak256("onApprovalReceived(address,uint256,bytes)"))` if approval is allowed unless throwing.
|
||||
*/
|
||||
function onApprovalReceived(address owner, uint256 amount, bytes memory data) external returns (bytes4);
|
||||
function onApprovalReceived(address owner, uint256 value, bytes calldata data) external returns (bytes4);
|
||||
}
|
||||
|
||||
14
contracts/mocks/token/ERC1363ForceApproveMock.sol
Normal file
14
contracts/mocks/token/ERC1363ForceApproveMock.sol
Normal file
@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC20} from "../../interfaces/IERC20.sol";
|
||||
import {ERC20, ERC1363} from "../../token/ERC20/extensions/ERC1363.sol";
|
||||
|
||||
// contract that replicate USDT approval behavior in approveAndCall
|
||||
abstract contract ERC1363ForceApproveMock is ERC1363 {
|
||||
function approveAndCall(address spender, uint256 amount, bytes memory data) public virtual override returns (bool) {
|
||||
require(amount == 0 || allowance(msg.sender, spender) == 0, "USDT approval failure");
|
||||
return super.approveAndCall(spender, amount, data);
|
||||
}
|
||||
}
|
||||
34
contracts/mocks/token/ERC1363NoReturnMock.sol
Normal file
34
contracts/mocks/token/ERC1363NoReturnMock.sol
Normal file
@ -0,0 +1,34 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC20, ERC20} from "../../token/ERC20/ERC20.sol";
|
||||
import {ERC1363} from "../../token/ERC20/extensions/ERC1363.sol";
|
||||
|
||||
abstract contract ERC1363NoReturnMock is ERC1363 {
|
||||
function transferAndCall(address to, uint256 value, bytes memory data) public override returns (bool) {
|
||||
super.transferAndCall(to, value, data);
|
||||
assembly {
|
||||
return(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function transferFromAndCall(
|
||||
address from,
|
||||
address to,
|
||||
uint256 value,
|
||||
bytes memory data
|
||||
) public override returns (bool) {
|
||||
super.transferFromAndCall(from, to, value, data);
|
||||
assembly {
|
||||
return(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function approveAndCall(address spender, uint256 value, bytes memory data) public override returns (bool) {
|
||||
super.approveAndCall(spender, value, data);
|
||||
assembly {
|
||||
return(0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
52
contracts/mocks/token/ERC1363ReceiverMock.sol
Normal file
52
contracts/mocks/token/ERC1363ReceiverMock.sol
Normal file
@ -0,0 +1,52 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC1363Receiver} from "../../interfaces/IERC1363Receiver.sol";
|
||||
|
||||
contract ERC1363ReceiverMock is IERC1363Receiver {
|
||||
enum RevertType {
|
||||
None,
|
||||
RevertWithoutMessage,
|
||||
RevertWithMessage,
|
||||
RevertWithCustomError,
|
||||
Panic
|
||||
}
|
||||
|
||||
bytes4 private _retval;
|
||||
RevertType private _error;
|
||||
|
||||
event Received(address operator, address from, uint256 value, bytes data);
|
||||
error CustomError(bytes4);
|
||||
|
||||
constructor() {
|
||||
_retval = IERC1363Receiver.onTransferReceived.selector;
|
||||
_error = RevertType.None;
|
||||
}
|
||||
|
||||
function setUp(bytes4 retval, RevertType error) public {
|
||||
_retval = retval;
|
||||
_error = error;
|
||||
}
|
||||
|
||||
function onTransferReceived(
|
||||
address operator,
|
||||
address from,
|
||||
uint256 value,
|
||||
bytes calldata data
|
||||
) external override returns (bytes4) {
|
||||
if (_error == RevertType.RevertWithoutMessage) {
|
||||
revert();
|
||||
} else if (_error == RevertType.RevertWithMessage) {
|
||||
revert("ERC1363ReceiverMock: reverting");
|
||||
} else if (_error == RevertType.RevertWithCustomError) {
|
||||
revert CustomError(_retval);
|
||||
} else if (_error == RevertType.Panic) {
|
||||
uint256 a = uint256(0) / uint256(0);
|
||||
a;
|
||||
}
|
||||
|
||||
emit Received(operator, from, value, data);
|
||||
return _retval;
|
||||
}
|
||||
}
|
||||
34
contracts/mocks/token/ERC1363ReturnFalseMock.sol
Normal file
34
contracts/mocks/token/ERC1363ReturnFalseMock.sol
Normal file
@ -0,0 +1,34 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC20, ERC20} from "../../token/ERC20/ERC20.sol";
|
||||
import {ERC1363} from "../../token/ERC20/extensions/ERC1363.sol";
|
||||
|
||||
abstract contract ERC1363ReturnFalseOnERC20Mock is ERC1363 {
|
||||
function transfer(address, uint256) public pure override(IERC20, ERC20) returns (bool) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function transferFrom(address, address, uint256) public pure override(IERC20, ERC20) returns (bool) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function approve(address, uint256) public pure override(IERC20, ERC20) returns (bool) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
abstract contract ERC1363ReturnFalseMock is ERC1363 {
|
||||
function transferAndCall(address, uint256, bytes memory) public pure override returns (bool) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function transferFromAndCall(address, address, uint256, bytes memory) public pure override returns (bool) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function approveAndCall(address, uint256, bytes memory) public pure override returns (bool) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
47
contracts/mocks/token/ERC1363SpenderMock.sol
Normal file
47
contracts/mocks/token/ERC1363SpenderMock.sol
Normal file
@ -0,0 +1,47 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC1363Spender} from "../../interfaces/IERC1363Spender.sol";
|
||||
|
||||
contract ERC1363SpenderMock is IERC1363Spender {
|
||||
enum RevertType {
|
||||
None,
|
||||
RevertWithoutMessage,
|
||||
RevertWithMessage,
|
||||
RevertWithCustomError,
|
||||
Panic
|
||||
}
|
||||
|
||||
bytes4 private _retval;
|
||||
RevertType private _error;
|
||||
|
||||
event Approved(address owner, uint256 value, bytes data);
|
||||
error CustomError(bytes4);
|
||||
|
||||
constructor() {
|
||||
_retval = IERC1363Spender.onApprovalReceived.selector;
|
||||
_error = RevertType.None;
|
||||
}
|
||||
|
||||
function setUp(bytes4 retval, RevertType error) public {
|
||||
_retval = retval;
|
||||
_error = error;
|
||||
}
|
||||
|
||||
function onApprovalReceived(address owner, uint256 value, bytes calldata data) external override returns (bytes4) {
|
||||
if (_error == RevertType.RevertWithoutMessage) {
|
||||
revert();
|
||||
} else if (_error == RevertType.RevertWithMessage) {
|
||||
revert("ERC1363SpenderMock: reverting");
|
||||
} else if (_error == RevertType.RevertWithCustomError) {
|
||||
revert CustomError(_retval);
|
||||
} else if (_error == RevertType.Panic) {
|
||||
uint256 a = uint256(0) / uint256(0);
|
||||
a;
|
||||
}
|
||||
|
||||
emit Approved(owner, value, data);
|
||||
return _retval;
|
||||
}
|
||||
}
|
||||
@ -22,6 +22,7 @@ Additionally there are multiple custom extensions, including:
|
||||
* {ERC20FlashMint}: token level support for flash loans through the minting and burning of ephemeral tokens (standardized as ERC-3156).
|
||||
* {ERC20Votes}: support for voting and vote delegation.
|
||||
* {ERC20Wrapper}: wrapper to create an ERC-20 backed by another ERC-20, with deposit and withdraw methods. Useful in conjunction with {ERC20Votes}.
|
||||
* {ERC1363}: support for calling the target of a transfer or approval, enabling code execution on the receiver within a single transaction.
|
||||
* {ERC4626}: tokenized vault that manages shares (represented as ERC-20) that are backed by assets (another ERC-20).
|
||||
|
||||
Finally, there are some utilities to interact with ERC-20 contracts in various ways:
|
||||
@ -60,6 +61,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel
|
||||
|
||||
{{ERC20FlashMint}}
|
||||
|
||||
{{ERC1363}}
|
||||
|
||||
{{ERC4626}}
|
||||
|
||||
== Utilities
|
||||
|
||||
198
contracts/token/ERC20/extensions/ERC1363.sol
Normal file
198
contracts/token/ERC20/extensions/ERC1363.sol
Normal file
@ -0,0 +1,198 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {ERC20} from "../ERC20.sol";
|
||||
import {IERC165, ERC165} from "../../../utils/introspection/ERC165.sol";
|
||||
|
||||
import {IERC1363} from "../../../interfaces/IERC1363.sol";
|
||||
import {IERC1363Receiver} from "../../../interfaces/IERC1363Receiver.sol";
|
||||
import {IERC1363Spender} from "../../../interfaces/IERC1363Spender.sol";
|
||||
|
||||
/**
|
||||
* @title ERC1363
|
||||
* @dev Extension of {ERC20} tokens that adds support for code execution after transfers and approvals
|
||||
* on recipient contracts. Calls after transfers are enabled through the {ERC1363-transferAndCall} and
|
||||
* {ERC1363-transferFromAndCall} methods while calls after approvals can be made with {ERC1363-approveAndCall}
|
||||
*/
|
||||
abstract contract ERC1363 is ERC20, ERC165, IERC1363 {
|
||||
/**
|
||||
* @dev Indicates a failure with the token `receiver`. Used in transfers.
|
||||
* @param receiver Address to which tokens are being transferred.
|
||||
*/
|
||||
error ERC1363InvalidReceiver(address receiver);
|
||||
|
||||
/**
|
||||
* @dev Indicates a failure with the token `spender`. Used in approvals.
|
||||
* @param spender Address that may be allowed to operate on tokens without being their owner.
|
||||
*/
|
||||
error ERC1363InvalidSpender(address spender);
|
||||
|
||||
/**
|
||||
* @dev Indicates a failure within the {transfer} part of a transferAndCall operation.
|
||||
*/
|
||||
error ERC1363TransferFailed(address to, uint256 value);
|
||||
|
||||
/**
|
||||
* @dev Indicates a failure within the {transferFrom} part of a transferFromAndCall operation.
|
||||
*/
|
||||
error ERC1363TransferFromFailed(address from, address to, uint256 value);
|
||||
|
||||
/**
|
||||
* @dev Indicates a failure within the {approve} part of a approveAndCall operation.
|
||||
*/
|
||||
error ERC1363ApproveFailed(address spender, uint256 value);
|
||||
|
||||
/**
|
||||
* @inheritdoc IERC165
|
||||
*/
|
||||
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
|
||||
return interfaceId == type(IERC1363).interfaceId || super.supportsInterface(interfaceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Moves a `value` amount of tokens from the caller's account to `to`
|
||||
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - The target has code (i.e. is a contract).
|
||||
* - The target `to` must implement the {IERC1363Receiver} interface.
|
||||
* - The target should return the {IERC1363Receiver} interface id.
|
||||
* - The internal {transfer} must succeed (returned `true`).
|
||||
*/
|
||||
function transferAndCall(address to, uint256 value) public returns (bool) {
|
||||
return transferAndCall(to, value, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Variant of {transferAndCall} that accepts an additional `data` parameter with
|
||||
* no specified format.
|
||||
*/
|
||||
function transferAndCall(address to, uint256 value, bytes memory data) public virtual returns (bool) {
|
||||
if (!transfer(to, value)) {
|
||||
revert ERC1363TransferFailed(to, value);
|
||||
}
|
||||
_checkOnTransferReceived(_msgSender(), to, value, data);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
|
||||
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - The target has code (i.e. is a contract).
|
||||
* - The target `to` must implement the {IERC1363Receiver} interface.
|
||||
* - The target should return the {IERC1363Receiver} interface id.
|
||||
* - The internal {transferFrom} must succeed (returned `true`).
|
||||
*/
|
||||
function transferFromAndCall(address from, address to, uint256 value) public returns (bool) {
|
||||
return transferFromAndCall(from, to, value, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Variant of {transferFromAndCall} that accepts an additional `data` parameter with
|
||||
* no specified format.
|
||||
*/
|
||||
function transferFromAndCall(
|
||||
address from,
|
||||
address to,
|
||||
uint256 value,
|
||||
bytes memory data
|
||||
) public virtual returns (bool) {
|
||||
if (!transferFrom(from, to, value)) {
|
||||
revert ERC1363TransferFromFailed(from, to, value);
|
||||
}
|
||||
_checkOnTransferReceived(from, to, value, data);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
|
||||
* caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - The target has code (i.e. is a contract).
|
||||
* - The target `to` must implement the {IERC1363Spender} interface.
|
||||
* - The target should return the {IERC1363Spender} interface id.
|
||||
* - The internal {approve} must succeed (returned `true`).
|
||||
*/
|
||||
function approveAndCall(address spender, uint256 value) public returns (bool) {
|
||||
return approveAndCall(spender, value, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Variant of {approveAndCall} that accepts an additional `data` parameter with
|
||||
* no specified format.
|
||||
*/
|
||||
function approveAndCall(address spender, uint256 value, bytes memory data) public virtual returns (bool) {
|
||||
if (!approve(spender, value)) {
|
||||
revert ERC1363ApproveFailed(spender, value);
|
||||
}
|
||||
_checkOnApprovalReceived(spender, value, data);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Performs a call to {IERC1363Receiver-onTransferReceived} on a target address.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - The target has code (i.e. is a contract).
|
||||
* - The target `to` must implement the {IERC1363Receiver} interface.
|
||||
* - The target should return the {IERC1363Receiver} interface id.
|
||||
*/
|
||||
function _checkOnTransferReceived(address from, address to, uint256 value, bytes memory data) private {
|
||||
if (to.code.length == 0) {
|
||||
revert ERC1363InvalidReceiver(to);
|
||||
}
|
||||
|
||||
try IERC1363Receiver(to).onTransferReceived(_msgSender(), from, value, data) returns (bytes4 retval) {
|
||||
if (retval != IERC1363Receiver.onTransferReceived.selector) {
|
||||
revert ERC1363InvalidReceiver(to);
|
||||
}
|
||||
} catch (bytes memory reason) {
|
||||
if (reason.length == 0) {
|
||||
revert ERC1363InvalidReceiver(to);
|
||||
} else {
|
||||
/// @solidity memory-safe-assembly
|
||||
assembly {
|
||||
revert(add(32, reason), mload(reason))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Performs a call to {IERC1363Spender-onApprovalReceived} on a target address.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - The target has code (i.e. is a contract).
|
||||
* - The target `to` must implement the {IERC1363Spender} interface.
|
||||
* - The target should return the {IERC1363Spender} interface id.
|
||||
*/
|
||||
function _checkOnApprovalReceived(address spender, uint256 value, bytes memory data) private {
|
||||
if (spender.code.length == 0) {
|
||||
revert ERC1363InvalidSpender(spender);
|
||||
}
|
||||
|
||||
try IERC1363Spender(spender).onApprovalReceived(_msgSender(), value, data) returns (bytes4 retval) {
|
||||
if (retval != IERC1363Spender.onApprovalReceived.selector) {
|
||||
revert ERC1363InvalidSpender(spender);
|
||||
}
|
||||
} catch (bytes memory reason) {
|
||||
if (reason.length == 0) {
|
||||
revert ERC1363InvalidSpender(spender);
|
||||
} else {
|
||||
/// @solidity memory-safe-assembly
|
||||
assembly {
|
||||
revert(add(32, reason), mload(reason))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,7 @@
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC20} from "../IERC20.sol";
|
||||
import {IERC20Permit} from "../extensions/IERC20Permit.sol";
|
||||
import {IERC1363} from "../../../interfaces/IERC1363.sol";
|
||||
import {Address} from "../../../utils/Address.sol";
|
||||
|
||||
/**
|
||||
@ -82,6 +82,61 @@ library SafeERC20 {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no
|
||||
* code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
|
||||
* targeting contracts.
|
||||
*
|
||||
* Reverts if the returned value is other than `true`.
|
||||
*/
|
||||
function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
|
||||
if (to.code.length == 0) {
|
||||
safeTransfer(token, to, value);
|
||||
} else if (!token.transferAndCall(to, value, data)) {
|
||||
revert SafeERC20FailedOperation(address(token));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target
|
||||
* has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
|
||||
* targeting contracts.
|
||||
*
|
||||
* Reverts if the returned value is other than `true`.
|
||||
*/
|
||||
function transferFromAndCallRelaxed(
|
||||
IERC1363 token,
|
||||
address from,
|
||||
address to,
|
||||
uint256 value,
|
||||
bytes memory data
|
||||
) internal {
|
||||
if (to.code.length == 0) {
|
||||
safeTransferFrom(token, from, to, value);
|
||||
} else if (!token.transferFromAndCall(from, to, value, data)) {
|
||||
revert SafeERC20FailedOperation(address(token));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no
|
||||
* code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
|
||||
* targeting contracts.
|
||||
*
|
||||
* NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}.
|
||||
* Opposedly, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall}
|
||||
* once without retrying, and relies on the returned value to be true.
|
||||
*
|
||||
* Reverts if the returned value is other than `true`.
|
||||
*/
|
||||
function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
|
||||
if (to.code.length == 0) {
|
||||
forceApprove(token, to, value);
|
||||
} else if (!token.approveAndCall(to, value, data)) {
|
||||
revert SafeERC20FailedOperation(address(token));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
|
||||
* on the return value: the return value is optional (but if data is returned, it must not be false).
|
||||
|
||||
@ -4,17 +4,21 @@ const { expect } = require('chai');
|
||||
function shouldBehaveLikeERC20(initialSupply, opts = {}) {
|
||||
const { forcedApproval } = opts;
|
||||
|
||||
beforeEach(async function () {
|
||||
[this.holder, this.recipient, this.other] = this.accounts;
|
||||
});
|
||||
|
||||
it('total supply: returns the total token value', async function () {
|
||||
expect(await this.token.totalSupply()).to.equal(initialSupply);
|
||||
});
|
||||
|
||||
describe('balanceOf', function () {
|
||||
it('returns zero when the requested account has no tokens', async function () {
|
||||
expect(await this.token.balanceOf(this.anotherAccount)).to.equal(0n);
|
||||
expect(await this.token.balanceOf(this.other)).to.equal(0n);
|
||||
});
|
||||
|
||||
it('returns the total token value when the requested account has some tokens', async function () {
|
||||
expect(await this.token.balanceOf(this.initialHolder)).to.equal(initialSupply);
|
||||
expect(await this.token.balanceOf(this.holder)).to.equal(initialSupply);
|
||||
});
|
||||
});
|
||||
|
||||
@ -31,34 +35,26 @@ function shouldBehaveLikeERC20(initialSupply, opts = {}) {
|
||||
describe('when the recipient is not the zero address', function () {
|
||||
describe('when the spender has enough allowance', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.connect(this.initialHolder).approve(this.recipient, initialSupply);
|
||||
await this.token.connect(this.holder).approve(this.recipient, initialSupply);
|
||||
});
|
||||
|
||||
describe('when the token owner has enough balance', function () {
|
||||
const value = initialSupply;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.tx = await this.token
|
||||
.connect(this.recipient)
|
||||
.transferFrom(this.initialHolder, this.anotherAccount, value);
|
||||
this.tx = await this.token.connect(this.recipient).transferFrom(this.holder, this.other, value);
|
||||
});
|
||||
|
||||
it('transfers the requested value', async function () {
|
||||
await expect(this.tx).to.changeTokenBalances(
|
||||
this.token,
|
||||
[this.initialHolder, this.anotherAccount],
|
||||
[-value, value],
|
||||
);
|
||||
await expect(this.tx).to.changeTokenBalances(this.token, [this.holder, this.other], [-value, value]);
|
||||
});
|
||||
|
||||
it('decreases the spender allowance', async function () {
|
||||
expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(0n);
|
||||
expect(await this.token.allowance(this.holder, this.recipient)).to.equal(0n);
|
||||
});
|
||||
|
||||
it('emits a transfer event', async function () {
|
||||
await expect(this.tx)
|
||||
.to.emit(this.token, 'Transfer')
|
||||
.withArgs(this.initialHolder, this.anotherAccount, value);
|
||||
await expect(this.tx).to.emit(this.token, 'Transfer').withArgs(this.holder, this.other, value);
|
||||
});
|
||||
|
||||
if (forcedApproval) {
|
||||
@ -66,9 +62,9 @@ function shouldBehaveLikeERC20(initialSupply, opts = {}) {
|
||||
await expect(this.tx)
|
||||
.to.emit(this.token, 'Approval')
|
||||
.withArgs(
|
||||
this.initialHolder.address,
|
||||
this.holder.address,
|
||||
this.recipient.address,
|
||||
await this.token.allowance(this.initialHolder, this.recipient),
|
||||
await this.token.allowance(this.holder, this.recipient),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
@ -80,12 +76,10 @@ function shouldBehaveLikeERC20(initialSupply, opts = {}) {
|
||||
|
||||
it('reverts when the token owner does not have enough balance', async function () {
|
||||
const value = initialSupply;
|
||||
await this.token.connect(this.initialHolder).transfer(this.anotherAccount, 1n);
|
||||
await expect(
|
||||
this.token.connect(this.recipient).transferFrom(this.initialHolder, this.anotherAccount, value),
|
||||
)
|
||||
await this.token.connect(this.holder).transfer(this.other, 1n);
|
||||
await expect(this.token.connect(this.recipient).transferFrom(this.holder, this.other, value))
|
||||
.to.revertedWithCustomError(this.token, 'ERC20InsufficientBalance')
|
||||
.withArgs(this.initialHolder, value - 1n, value);
|
||||
.withArgs(this.holder, value - 1n, value);
|
||||
});
|
||||
});
|
||||
|
||||
@ -93,39 +87,33 @@ function shouldBehaveLikeERC20(initialSupply, opts = {}) {
|
||||
const allowance = initialSupply - 1n;
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.token.connect(this.initialHolder).approve(this.recipient, allowance);
|
||||
await this.token.connect(this.holder).approve(this.recipient, allowance);
|
||||
});
|
||||
|
||||
it('reverts when the token owner has enough balance', async function () {
|
||||
const value = initialSupply;
|
||||
await expect(
|
||||
this.token.connect(this.recipient).transferFrom(this.initialHolder, this.anotherAccount, value),
|
||||
)
|
||||
await expect(this.token.connect(this.recipient).transferFrom(this.holder, this.other, value))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC20InsufficientAllowance')
|
||||
.withArgs(this.recipient, allowance, value);
|
||||
});
|
||||
|
||||
it('reverts when the token owner does not have enough balance', async function () {
|
||||
const value = allowance;
|
||||
await this.token.connect(this.initialHolder).transfer(this.anotherAccount, 2);
|
||||
await expect(
|
||||
this.token.connect(this.recipient).transferFrom(this.initialHolder, this.anotherAccount, value),
|
||||
)
|
||||
await this.token.connect(this.holder).transfer(this.other, 2);
|
||||
await expect(this.token.connect(this.recipient).transferFrom(this.holder, this.other, value))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance')
|
||||
.withArgs(this.initialHolder, value - 1n, value);
|
||||
.withArgs(this.holder, value - 1n, value);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the spender has unlimited allowance', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.connect(this.initialHolder).approve(this.recipient, ethers.MaxUint256);
|
||||
this.tx = await this.token
|
||||
.connect(this.recipient)
|
||||
.transferFrom(this.initialHolder, this.anotherAccount, 1n);
|
||||
await this.token.connect(this.holder).approve(this.recipient, ethers.MaxUint256);
|
||||
this.tx = await this.token.connect(this.recipient).transferFrom(this.holder, this.other, 1n);
|
||||
});
|
||||
|
||||
it('does not decrease the spender allowance', async function () {
|
||||
expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(ethers.MaxUint256);
|
||||
expect(await this.token.allowance(this.holder, this.recipient)).to.equal(ethers.MaxUint256);
|
||||
});
|
||||
|
||||
it('does not emit an approval event', async function () {
|
||||
@ -136,8 +124,8 @@ function shouldBehaveLikeERC20(initialSupply, opts = {}) {
|
||||
|
||||
it('reverts when the recipient is the zero address', async function () {
|
||||
const value = initialSupply;
|
||||
await this.token.connect(this.initialHolder).approve(this.recipient, value);
|
||||
await expect(this.token.connect(this.recipient).transferFrom(this.initialHolder, ethers.ZeroAddress, value))
|
||||
await this.token.connect(this.holder).approve(this.recipient, value);
|
||||
await expect(this.token.connect(this.recipient).transferFrom(this.holder, ethers.ZeroAddress, value))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver')
|
||||
.withArgs(ethers.ZeroAddress);
|
||||
});
|
||||
@ -164,24 +152,24 @@ function shouldBehaveLikeERC20Transfer(balance) {
|
||||
describe('when the recipient is not the zero address', function () {
|
||||
it('reverts when the sender does not have enough balance', async function () {
|
||||
const value = balance + 1n;
|
||||
await expect(this.transfer(this.initialHolder, this.recipient, value))
|
||||
await expect(this.transfer(this.holder, this.recipient, value))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance')
|
||||
.withArgs(this.initialHolder, balance, value);
|
||||
.withArgs(this.holder, balance, value);
|
||||
});
|
||||
|
||||
describe('when the sender transfers all balance', function () {
|
||||
const value = balance;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.tx = await this.transfer(this.initialHolder, this.recipient, value);
|
||||
this.tx = await this.transfer(this.holder, this.recipient, value);
|
||||
});
|
||||
|
||||
it('transfers the requested value', async function () {
|
||||
await expect(this.tx).to.changeTokenBalances(this.token, [this.initialHolder, this.recipient], [-value, value]);
|
||||
await expect(this.tx).to.changeTokenBalances(this.token, [this.holder, this.recipient], [-value, value]);
|
||||
});
|
||||
|
||||
it('emits a transfer event', async function () {
|
||||
await expect(this.tx).to.emit(this.token, 'Transfer').withArgs(this.initialHolder, this.recipient, value);
|
||||
await expect(this.tx).to.emit(this.token, 'Transfer').withArgs(this.holder, this.recipient, value);
|
||||
});
|
||||
});
|
||||
|
||||
@ -189,21 +177,21 @@ function shouldBehaveLikeERC20Transfer(balance) {
|
||||
const value = 0n;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.tx = await this.transfer(this.initialHolder, this.recipient, value);
|
||||
this.tx = await this.transfer(this.holder, this.recipient, value);
|
||||
});
|
||||
|
||||
it('transfers the requested value', async function () {
|
||||
await expect(this.tx).to.changeTokenBalances(this.token, [this.initialHolder, this.recipient], [0n, 0n]);
|
||||
await expect(this.tx).to.changeTokenBalances(this.token, [this.holder, this.recipient], [0n, 0n]);
|
||||
});
|
||||
|
||||
it('emits a transfer event', async function () {
|
||||
await expect(this.tx).to.emit(this.token, 'Transfer').withArgs(this.initialHolder, this.recipient, value);
|
||||
await expect(this.tx).to.emit(this.token, 'Transfer').withArgs(this.holder, this.recipient, value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('reverts when the recipient is the zero address', async function () {
|
||||
await expect(this.transfer(this.initialHolder, ethers.ZeroAddress, balance))
|
||||
await expect(this.transfer(this.holder, ethers.ZeroAddress, balance))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver')
|
||||
.withArgs(ethers.ZeroAddress);
|
||||
});
|
||||
@ -215,22 +203,22 @@ function shouldBehaveLikeERC20Approve(supply) {
|
||||
const value = supply;
|
||||
|
||||
it('emits an approval event', async function () {
|
||||
await expect(this.approve(this.initialHolder, this.recipient, value))
|
||||
await expect(this.approve(this.holder, this.recipient, value))
|
||||
.to.emit(this.token, 'Approval')
|
||||
.withArgs(this.initialHolder, this.recipient, value);
|
||||
.withArgs(this.holder, this.recipient, value);
|
||||
});
|
||||
|
||||
it('approves the requested value when there was no approved value before', async function () {
|
||||
await this.approve(this.initialHolder, this.recipient, value);
|
||||
await this.approve(this.holder, this.recipient, value);
|
||||
|
||||
expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(value);
|
||||
expect(await this.token.allowance(this.holder, this.recipient)).to.equal(value);
|
||||
});
|
||||
|
||||
it('approves the requested value and replaces the previous one when the spender had an approved value', async function () {
|
||||
await this.approve(this.initialHolder, this.recipient, 1n);
|
||||
await this.approve(this.initialHolder, this.recipient, value);
|
||||
await this.approve(this.holder, this.recipient, 1n);
|
||||
await this.approve(this.holder, this.recipient, value);
|
||||
|
||||
expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(value);
|
||||
expect(await this.token.allowance(this.holder, this.recipient)).to.equal(value);
|
||||
});
|
||||
});
|
||||
|
||||
@ -238,28 +226,28 @@ function shouldBehaveLikeERC20Approve(supply) {
|
||||
const value = supply + 1n;
|
||||
|
||||
it('emits an approval event', async function () {
|
||||
await expect(this.approve(this.initialHolder, this.recipient, value))
|
||||
await expect(this.approve(this.holder, this.recipient, value))
|
||||
.to.emit(this.token, 'Approval')
|
||||
.withArgs(this.initialHolder, this.recipient, value);
|
||||
.withArgs(this.holder, this.recipient, value);
|
||||
});
|
||||
|
||||
it('approves the requested value when there was no approved value before', async function () {
|
||||
await this.approve(this.initialHolder, this.recipient, value);
|
||||
await this.approve(this.holder, this.recipient, value);
|
||||
|
||||
expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(value);
|
||||
expect(await this.token.allowance(this.holder, this.recipient)).to.equal(value);
|
||||
});
|
||||
|
||||
it('approves the requested value and replaces the previous one when the spender had an approved value', async function () {
|
||||
await this.approve(this.initialHolder, this.recipient, 1n);
|
||||
await this.approve(this.initialHolder, this.recipient, value);
|
||||
await this.approve(this.holder, this.recipient, 1n);
|
||||
await this.approve(this.holder, this.recipient, value);
|
||||
|
||||
expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(value);
|
||||
expect(await this.token.allowance(this.holder, this.recipient)).to.equal(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('reverts when the spender is the zero address', async function () {
|
||||
await expect(this.approve(this.initialHolder, ethers.ZeroAddress, supply))
|
||||
await expect(this.approve(this.holder, ethers.ZeroAddress, supply))
|
||||
.to.be.revertedWithCustomError(this.token, `ERC20InvalidSpender`)
|
||||
.withArgs(ethers.ZeroAddress);
|
||||
});
|
||||
|
||||
@ -19,12 +19,14 @@ describe('ERC20', function () {
|
||||
for (const { Token, forcedApproval } of TOKENS) {
|
||||
describe(Token, function () {
|
||||
const fixture = async () => {
|
||||
const [initialHolder, recipient, anotherAccount] = await ethers.getSigners();
|
||||
// this.accounts is used by shouldBehaveLikeERC20
|
||||
const accounts = await ethers.getSigners();
|
||||
const [holder, recipient] = accounts;
|
||||
|
||||
const token = await ethers.deployContract(Token, [name, symbol]);
|
||||
await token.$_mint(initialHolder, initialSupply);
|
||||
await token.$_mint(holder, initialSupply);
|
||||
|
||||
return { initialHolder, recipient, anotherAccount, token };
|
||||
return { accounts, holder, recipient, token };
|
||||
};
|
||||
|
||||
beforeEach(async function () {
|
||||
@ -87,29 +89,27 @@ describe('ERC20', function () {
|
||||
|
||||
describe('for a non zero account', function () {
|
||||
it('rejects burning more than balance', async function () {
|
||||
await expect(this.token.$_burn(this.initialHolder, initialSupply + 1n))
|
||||
await expect(this.token.$_burn(this.holder, initialSupply + 1n))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance')
|
||||
.withArgs(this.initialHolder, initialSupply, initialSupply + 1n);
|
||||
.withArgs(this.holder, initialSupply, initialSupply + 1n);
|
||||
});
|
||||
|
||||
const describeBurn = function (description, value) {
|
||||
describe(description, function () {
|
||||
beforeEach('burning', async function () {
|
||||
this.tx = await this.token.$_burn(this.initialHolder, value);
|
||||
this.tx = await this.token.$_burn(this.holder, value);
|
||||
});
|
||||
|
||||
it('decrements totalSupply', async function () {
|
||||
expect(await this.token.totalSupply()).to.equal(initialSupply - value);
|
||||
});
|
||||
|
||||
it('decrements initialHolder balance', async function () {
|
||||
await expect(this.tx).to.changeTokenBalance(this.token, this.initialHolder, -value);
|
||||
it('decrements holder balance', async function () {
|
||||
await expect(this.tx).to.changeTokenBalance(this.token, this.holder, -value);
|
||||
});
|
||||
|
||||
it('emits Transfer event', async function () {
|
||||
await expect(this.tx)
|
||||
.to.emit(this.token, 'Transfer')
|
||||
.withArgs(this.initialHolder, ethers.ZeroAddress, value);
|
||||
await expect(this.tx).to.emit(this.token, 'Transfer').withArgs(this.holder, ethers.ZeroAddress, value);
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -127,19 +127,19 @@ describe('ERC20', function () {
|
||||
});
|
||||
|
||||
it('from is the zero address', async function () {
|
||||
const tx = await this.token.$_update(ethers.ZeroAddress, this.initialHolder, value);
|
||||
await expect(tx).to.emit(this.token, 'Transfer').withArgs(ethers.ZeroAddress, this.initialHolder, value);
|
||||
const tx = await this.token.$_update(ethers.ZeroAddress, this.holder, value);
|
||||
await expect(tx).to.emit(this.token, 'Transfer').withArgs(ethers.ZeroAddress, this.holder, value);
|
||||
|
||||
expect(await this.token.totalSupply()).to.equal(this.totalSupply + value);
|
||||
await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, value);
|
||||
await expect(tx).to.changeTokenBalance(this.token, this.holder, value);
|
||||
});
|
||||
|
||||
it('to is the zero address', async function () {
|
||||
const tx = await this.token.$_update(this.initialHolder, ethers.ZeroAddress, value);
|
||||
await expect(tx).to.emit(this.token, 'Transfer').withArgs(this.initialHolder, ethers.ZeroAddress, value);
|
||||
const tx = await this.token.$_update(this.holder, ethers.ZeroAddress, value);
|
||||
await expect(tx).to.emit(this.token, 'Transfer').withArgs(this.holder, ethers.ZeroAddress, value);
|
||||
|
||||
expect(await this.token.totalSupply()).to.equal(this.totalSupply - value);
|
||||
await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, -value);
|
||||
await expect(tx).to.changeTokenBalance(this.token, this.holder, -value);
|
||||
});
|
||||
|
||||
describe('from and to are the same address', function () {
|
||||
@ -159,9 +159,9 @@ describe('ERC20', function () {
|
||||
});
|
||||
|
||||
it('executes with balance', async function () {
|
||||
const tx = await this.token.$_update(this.initialHolder, this.initialHolder, value);
|
||||
await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, 0n);
|
||||
await expect(tx).to.emit(this.token, 'Transfer').withArgs(this.initialHolder, this.initialHolder, value);
|
||||
const tx = await this.token.$_update(this.holder, this.holder, value);
|
||||
await expect(tx).to.changeTokenBalance(this.token, this.holder, 0n);
|
||||
await expect(tx).to.emit(this.token, 'Transfer').withArgs(this.holder, this.holder, value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
370
test/token/ERC20/extensions/ERC1363.test.js
Normal file
370
test/token/ERC20/extensions/ERC1363.test.js
Normal file
@ -0,0 +1,370 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const {
|
||||
shouldBehaveLikeERC20,
|
||||
shouldBehaveLikeERC20Transfer,
|
||||
shouldBehaveLikeERC20Approve,
|
||||
} = require('../ERC20.behavior.js');
|
||||
const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior');
|
||||
const { RevertType } = require('../../../helpers/enums.js');
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
const value = 1000n;
|
||||
const data = '0x123456';
|
||||
|
||||
async function fixture() {
|
||||
// this.accounts is used by shouldBehaveLikeERC20
|
||||
const accounts = await ethers.getSigners();
|
||||
const [holder, other] = accounts;
|
||||
|
||||
const receiver = await ethers.deployContract('ERC1363ReceiverMock');
|
||||
const spender = await ethers.deployContract('ERC1363SpenderMock');
|
||||
const token = await ethers.deployContract('$ERC1363', [name, symbol]);
|
||||
|
||||
await token.$_mint(holder, value);
|
||||
|
||||
return {
|
||||
accounts,
|
||||
holder,
|
||||
other,
|
||||
token,
|
||||
receiver,
|
||||
spender,
|
||||
selectors: {
|
||||
onTransferReceived: receiver.interface.getFunction('onTransferReceived(address,address,uint256,bytes)').selector,
|
||||
onApprovalReceived: spender.interface.getFunction('onApprovalReceived(address,uint256,bytes)').selector,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('ERC1363', function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
shouldSupportInterfaces(['ERC165', 'ERC1363']);
|
||||
shouldBehaveLikeERC20(value);
|
||||
|
||||
describe('transferAndCall', function () {
|
||||
describe('as a transfer', function () {
|
||||
beforeEach(async function () {
|
||||
this.recipient = this.receiver;
|
||||
this.transfer = (holder, ...rest) =>
|
||||
this.token.connect(holder).getFunction('transferAndCall(address,uint256)')(...rest);
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC20Transfer(value);
|
||||
});
|
||||
|
||||
it('reverts transferring to an EOA', async function () {
|
||||
await expect(this.token.connect(this.holder).getFunction('transferAndCall(address,uint256)')(this.other, value))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver')
|
||||
.withArgs(this.other.address);
|
||||
});
|
||||
|
||||
it('succeeds without data', async function () {
|
||||
await expect(
|
||||
this.token.connect(this.holder).getFunction('transferAndCall(address,uint256)')(this.receiver, value),
|
||||
)
|
||||
.to.emit(this.token, 'Transfer')
|
||||
.withArgs(this.holder.address, this.receiver.target, value)
|
||||
.to.emit(this.receiver, 'Received')
|
||||
.withArgs(this.holder.address, this.holder.address, value, '0x');
|
||||
});
|
||||
|
||||
it('succeeds with data', async function () {
|
||||
await expect(
|
||||
this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(
|
||||
this.receiver,
|
||||
value,
|
||||
data,
|
||||
),
|
||||
)
|
||||
.to.emit(this.token, 'Transfer')
|
||||
.withArgs(this.holder.address, this.receiver.target, value)
|
||||
.to.emit(this.receiver, 'Received')
|
||||
.withArgs(this.holder.address, this.holder.address, value, data);
|
||||
});
|
||||
|
||||
it('reverts with reverting hook (without reason)', async function () {
|
||||
await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.RevertWithoutMessage);
|
||||
|
||||
await expect(
|
||||
this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(
|
||||
this.receiver,
|
||||
value,
|
||||
data,
|
||||
),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver')
|
||||
.withArgs(this.receiver.target);
|
||||
});
|
||||
|
||||
it('reverts with reverting hook (with reason)', async function () {
|
||||
await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.RevertWithMessage);
|
||||
|
||||
await expect(
|
||||
this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(
|
||||
this.receiver,
|
||||
value,
|
||||
data,
|
||||
),
|
||||
).to.be.revertedWith('ERC1363ReceiverMock: reverting');
|
||||
});
|
||||
|
||||
it('reverts with reverting hook (with custom error)', async function () {
|
||||
const reason = '0x12345678';
|
||||
await this.receiver.setUp(reason, RevertType.RevertWithCustomError);
|
||||
|
||||
await expect(
|
||||
this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(
|
||||
this.receiver,
|
||||
value,
|
||||
data,
|
||||
),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.receiver, 'CustomError')
|
||||
.withArgs(reason);
|
||||
});
|
||||
|
||||
it('panics with reverting hook (with panic)', async function () {
|
||||
await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.Panic);
|
||||
|
||||
await expect(
|
||||
this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(
|
||||
this.receiver,
|
||||
value,
|
||||
data,
|
||||
),
|
||||
).to.be.revertedWithPanic();
|
||||
});
|
||||
|
||||
it('reverts with bad return value', async function () {
|
||||
await this.receiver.setUp('0x12345678', RevertType.None);
|
||||
|
||||
await expect(
|
||||
this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(
|
||||
this.receiver,
|
||||
value,
|
||||
data,
|
||||
),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver')
|
||||
.withArgs(this.receiver.target);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transferFromAndCall', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.connect(this.holder).approve(this.other, ethers.MaxUint256);
|
||||
});
|
||||
|
||||
describe('as a transfer', function () {
|
||||
beforeEach(async function () {
|
||||
this.recipient = this.receiver;
|
||||
this.transfer = this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256)');
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC20Transfer(value);
|
||||
});
|
||||
|
||||
it('reverts transferring to an EOA', async function () {
|
||||
await expect(
|
||||
this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256)')(
|
||||
this.holder,
|
||||
this.other,
|
||||
value,
|
||||
),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver')
|
||||
.withArgs(this.other.address);
|
||||
});
|
||||
|
||||
it('succeeds without data', async function () {
|
||||
await expect(
|
||||
this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256)')(
|
||||
this.holder,
|
||||
this.receiver,
|
||||
value,
|
||||
),
|
||||
)
|
||||
.to.emit(this.token, 'Transfer')
|
||||
.withArgs(this.holder.address, this.receiver.target, value)
|
||||
.to.emit(this.receiver, 'Received')
|
||||
.withArgs(this.other.address, this.holder.address, value, '0x');
|
||||
});
|
||||
|
||||
it('succeeds with data', async function () {
|
||||
await expect(
|
||||
this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(
|
||||
this.holder,
|
||||
this.receiver,
|
||||
value,
|
||||
data,
|
||||
),
|
||||
)
|
||||
.to.emit(this.token, 'Transfer')
|
||||
.withArgs(this.holder.address, this.receiver.target, value)
|
||||
.to.emit(this.receiver, 'Received')
|
||||
.withArgs(this.other.address, this.holder.address, value, data);
|
||||
});
|
||||
|
||||
it('reverts with reverting hook (without reason)', async function () {
|
||||
await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.RevertWithoutMessage);
|
||||
|
||||
await expect(
|
||||
this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(
|
||||
this.holder,
|
||||
this.receiver,
|
||||
value,
|
||||
data,
|
||||
),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver')
|
||||
.withArgs(this.receiver.target);
|
||||
});
|
||||
|
||||
it('reverts with reverting hook (with reason)', async function () {
|
||||
await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.RevertWithMessage);
|
||||
|
||||
await expect(
|
||||
this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(
|
||||
this.holder,
|
||||
this.receiver,
|
||||
value,
|
||||
data,
|
||||
),
|
||||
).to.be.revertedWith('ERC1363ReceiverMock: reverting');
|
||||
});
|
||||
|
||||
it('reverts with reverting hook (with custom error)', async function () {
|
||||
const reason = '0x12345678';
|
||||
await this.receiver.setUp(reason, RevertType.RevertWithCustomError);
|
||||
|
||||
await expect(
|
||||
this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(
|
||||
this.holder,
|
||||
this.receiver,
|
||||
value,
|
||||
data,
|
||||
),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.receiver, 'CustomError')
|
||||
.withArgs(reason);
|
||||
});
|
||||
|
||||
it('panics with reverting hook (with panic)', async function () {
|
||||
await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.Panic);
|
||||
|
||||
await expect(
|
||||
this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(
|
||||
this.holder,
|
||||
this.receiver,
|
||||
value,
|
||||
data,
|
||||
),
|
||||
).to.be.revertedWithPanic();
|
||||
});
|
||||
|
||||
it('reverts with bad return value', async function () {
|
||||
await this.receiver.setUp('0x12345678', RevertType.None);
|
||||
|
||||
await expect(
|
||||
this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(
|
||||
this.holder,
|
||||
this.receiver,
|
||||
value,
|
||||
data,
|
||||
),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver')
|
||||
.withArgs(this.receiver.target);
|
||||
});
|
||||
});
|
||||
|
||||
describe('approveAndCall', function () {
|
||||
describe('as an approval', function () {
|
||||
beforeEach(async function () {
|
||||
this.recipient = this.spender;
|
||||
this.approve = (holder, ...rest) =>
|
||||
this.token.connect(holder).getFunction('approveAndCall(address,uint256)')(...rest);
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC20Approve(value);
|
||||
});
|
||||
|
||||
it('reverts approving an EOA', async function () {
|
||||
await expect(this.token.connect(this.holder).getFunction('approveAndCall(address,uint256)')(this.other, value))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1363InvalidSpender')
|
||||
.withArgs(this.other.address);
|
||||
});
|
||||
|
||||
it('succeeds without data', async function () {
|
||||
await expect(this.token.connect(this.holder).getFunction('approveAndCall(address,uint256)')(this.spender, value))
|
||||
.to.emit(this.token, 'Approval')
|
||||
.withArgs(this.holder.address, this.spender.target, value)
|
||||
.to.emit(this.spender, 'Approved')
|
||||
.withArgs(this.holder.address, value, '0x');
|
||||
});
|
||||
|
||||
it('succeeds with data', async function () {
|
||||
await expect(
|
||||
this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data),
|
||||
)
|
||||
.to.emit(this.token, 'Approval')
|
||||
.withArgs(this.holder.address, this.spender.target, value)
|
||||
.to.emit(this.spender, 'Approved')
|
||||
.withArgs(this.holder.address, value, data);
|
||||
});
|
||||
|
||||
it('with reverting hook (without reason)', async function () {
|
||||
await this.spender.setUp(this.selectors.onApprovalReceived, RevertType.RevertWithoutMessage);
|
||||
|
||||
await expect(
|
||||
this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1363InvalidSpender')
|
||||
.withArgs(this.spender.target);
|
||||
});
|
||||
|
||||
it('reverts with reverting hook (with reason)', async function () {
|
||||
await this.spender.setUp(this.selectors.onApprovalReceived, RevertType.RevertWithMessage);
|
||||
|
||||
await expect(
|
||||
this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data),
|
||||
).to.be.revertedWith('ERC1363SpenderMock: reverting');
|
||||
});
|
||||
|
||||
it('reverts with reverting hook (with custom error)', async function () {
|
||||
const reason = '0x12345678';
|
||||
await this.spender.setUp(reason, RevertType.RevertWithCustomError);
|
||||
|
||||
await expect(
|
||||
this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.spender, 'CustomError')
|
||||
.withArgs(reason);
|
||||
});
|
||||
|
||||
it('panics with reverting hook (with panic)', async function () {
|
||||
await this.spender.setUp(this.selectors.onApprovalReceived, RevertType.Panic);
|
||||
|
||||
await expect(
|
||||
this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data),
|
||||
).to.be.revertedWithPanic();
|
||||
});
|
||||
|
||||
it('reverts with bad return value', async function () {
|
||||
await this.spender.setUp('0x12345678', RevertType.None);
|
||||
|
||||
await expect(
|
||||
this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1363InvalidSpender')
|
||||
.withArgs(this.spender.target);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -8,12 +8,12 @@ const initialSupply = 100n;
|
||||
const loanValue = 10_000_000_000_000n;
|
||||
|
||||
async function fixture() {
|
||||
const [initialHolder, other, anotherAccount] = await ethers.getSigners();
|
||||
const [holder, other] = await ethers.getSigners();
|
||||
|
||||
const token = await ethers.deployContract('$ERC20FlashMintMock', [name, symbol]);
|
||||
await token.$_mint(initialHolder, initialSupply);
|
||||
await token.$_mint(holder, initialSupply);
|
||||
|
||||
return { initialHolder, other, anotherAccount, token };
|
||||
return { holder, other, token };
|
||||
}
|
||||
|
||||
describe('ERC20FlashMint', function () {
|
||||
@ -134,7 +134,7 @@ describe('ERC20FlashMint', function () {
|
||||
});
|
||||
|
||||
it('custom flash fee receiver', async function () {
|
||||
const flashFeeReceiverAddress = this.anotherAccount;
|
||||
const flashFeeReceiverAddress = this.other;
|
||||
await this.token.setFlashFeeReceiver(flashFeeReceiverAddress);
|
||||
expect(await this.token.$_flashFeeReceiver()).to.equal(flashFeeReceiverAddress);
|
||||
|
||||
|
||||
@ -10,13 +10,13 @@ const symbol = 'MTKN';
|
||||
const initialSupply = 100n;
|
||||
|
||||
async function fixture() {
|
||||
const [initialHolder, spender, owner, other] = await ethers.getSigners();
|
||||
const [holder, spender, owner, other] = await ethers.getSigners();
|
||||
|
||||
const token = await ethers.deployContract('$ERC20Permit', [name, symbol, name]);
|
||||
await token.$_mint(initialHolder, initialSupply);
|
||||
await token.$_mint(holder, initialSupply);
|
||||
|
||||
return {
|
||||
initialHolder,
|
||||
holder,
|
||||
spender,
|
||||
owner,
|
||||
other,
|
||||
@ -30,7 +30,7 @@ describe('ERC20Permit', function () {
|
||||
});
|
||||
|
||||
it('initial nonce is 0', async function () {
|
||||
expect(await this.token.nonces(this.initialHolder)).to.equal(0n);
|
||||
expect(await this.token.nonces(this.holder)).to.equal(0n);
|
||||
});
|
||||
|
||||
it('domain separator', async function () {
|
||||
|
||||
@ -10,14 +10,16 @@ const decimals = 9n;
|
||||
const initialSupply = 100n;
|
||||
|
||||
async function fixture() {
|
||||
const [initialHolder, recipient, anotherAccount] = await ethers.getSigners();
|
||||
// this.accounts is used by shouldBehaveLikeERC20
|
||||
const accounts = await ethers.getSigners();
|
||||
const [holder, recipient, other] = accounts;
|
||||
|
||||
const underlying = await ethers.deployContract('$ERC20DecimalsMock', [name, symbol, decimals]);
|
||||
await underlying.$_mint(initialHolder, initialSupply);
|
||||
await underlying.$_mint(holder, initialSupply);
|
||||
|
||||
const token = await ethers.deployContract('$ERC20Wrapper', [`Wrapped ${name}`, `W${symbol}`, underlying]);
|
||||
|
||||
return { initialHolder, recipient, anotherAccount, underlying, token };
|
||||
return { accounts, holder, recipient, other, underlying, token };
|
||||
}
|
||||
|
||||
describe('ERC20Wrapper', function () {
|
||||
@ -53,57 +55,57 @@ describe('ERC20Wrapper', function () {
|
||||
|
||||
describe('deposit', function () {
|
||||
it('executes with approval', async function () {
|
||||
await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply);
|
||||
await this.underlying.connect(this.holder).approve(this.token, initialSupply);
|
||||
|
||||
const tx = await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply);
|
||||
const tx = await this.token.connect(this.holder).depositFor(this.holder, initialSupply);
|
||||
await expect(tx)
|
||||
.to.emit(this.underlying, 'Transfer')
|
||||
.withArgs(this.initialHolder, this.token, initialSupply)
|
||||
.withArgs(this.holder, this.token, initialSupply)
|
||||
.to.emit(this.token, 'Transfer')
|
||||
.withArgs(ethers.ZeroAddress, this.initialHolder, initialSupply);
|
||||
.withArgs(ethers.ZeroAddress, this.holder, initialSupply);
|
||||
await expect(tx).to.changeTokenBalances(
|
||||
this.underlying,
|
||||
[this.initialHolder, this.token],
|
||||
[this.holder, this.token],
|
||||
[-initialSupply, initialSupply],
|
||||
);
|
||||
await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, initialSupply);
|
||||
await expect(tx).to.changeTokenBalance(this.token, this.holder, initialSupply);
|
||||
});
|
||||
|
||||
it('reverts when missing approval', async function () {
|
||||
await expect(this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply))
|
||||
await expect(this.token.connect(this.holder).depositFor(this.holder, initialSupply))
|
||||
.to.be.revertedWithCustomError(this.underlying, 'ERC20InsufficientAllowance')
|
||||
.withArgs(this.token, 0, initialSupply);
|
||||
});
|
||||
|
||||
it('reverts when inssuficient balance', async function () {
|
||||
await this.underlying.connect(this.initialHolder).approve(this.token, ethers.MaxUint256);
|
||||
await this.underlying.connect(this.holder).approve(this.token, ethers.MaxUint256);
|
||||
|
||||
await expect(this.token.connect(this.initialHolder).depositFor(this.initialHolder, ethers.MaxUint256))
|
||||
await expect(this.token.connect(this.holder).depositFor(this.holder, ethers.MaxUint256))
|
||||
.to.be.revertedWithCustomError(this.underlying, 'ERC20InsufficientBalance')
|
||||
.withArgs(this.initialHolder, initialSupply, ethers.MaxUint256);
|
||||
.withArgs(this.holder, initialSupply, ethers.MaxUint256);
|
||||
});
|
||||
|
||||
it('deposits to other account', async function () {
|
||||
await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply);
|
||||
await this.underlying.connect(this.holder).approve(this.token, initialSupply);
|
||||
|
||||
const tx = await this.token.connect(this.initialHolder).depositFor(this.recipient, initialSupply);
|
||||
const tx = await this.token.connect(this.holder).depositFor(this.recipient, initialSupply);
|
||||
await expect(tx)
|
||||
.to.emit(this.underlying, 'Transfer')
|
||||
.withArgs(this.initialHolder, this.token, initialSupply)
|
||||
.withArgs(this.holder, this.token.target, initialSupply)
|
||||
.to.emit(this.token, 'Transfer')
|
||||
.withArgs(ethers.ZeroAddress, this.recipient, initialSupply);
|
||||
await expect(tx).to.changeTokenBalances(
|
||||
this.underlying,
|
||||
[this.initialHolder, this.token],
|
||||
[this.holder, this.token],
|
||||
[-initialSupply, initialSupply],
|
||||
);
|
||||
await expect(tx).to.changeTokenBalances(this.token, [this.initialHolder, this.recipient], [0, initialSupply]);
|
||||
await expect(tx).to.changeTokenBalances(this.token, [this.holder, this.recipient], [0, initialSupply]);
|
||||
});
|
||||
|
||||
it('reverts minting to the wrapper contract', async function () {
|
||||
await this.underlying.connect(this.initialHolder).approve(this.token, ethers.MaxUint256);
|
||||
await this.underlying.connect(this.holder).approve(this.token, ethers.MaxUint256);
|
||||
|
||||
await expect(this.token.connect(this.initialHolder).depositFor(this.token, ethers.MaxUint256))
|
||||
await expect(this.token.connect(this.holder).depositFor(this.token, ethers.MaxUint256))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver')
|
||||
.withArgs(this.token);
|
||||
});
|
||||
@ -111,61 +113,61 @@ describe('ERC20Wrapper', function () {
|
||||
|
||||
describe('withdraw', function () {
|
||||
beforeEach(async function () {
|
||||
await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply);
|
||||
await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply);
|
||||
await this.underlying.connect(this.holder).approve(this.token, initialSupply);
|
||||
await this.token.connect(this.holder).depositFor(this.holder, initialSupply);
|
||||
});
|
||||
|
||||
it('reverts when inssuficient balance', async function () {
|
||||
await expect(this.token.connect(this.initialHolder).withdrawTo(this.initialHolder, ethers.MaxInt256))
|
||||
await expect(this.token.connect(this.holder).withdrawTo(this.holder, ethers.MaxInt256))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance')
|
||||
.withArgs(this.initialHolder, initialSupply, ethers.MaxInt256);
|
||||
.withArgs(this.holder, initialSupply, ethers.MaxInt256);
|
||||
});
|
||||
|
||||
it('executes when operation is valid', async function () {
|
||||
const value = 42n;
|
||||
|
||||
const tx = await this.token.connect(this.initialHolder).withdrawTo(this.initialHolder, value);
|
||||
const tx = await this.token.connect(this.holder).withdrawTo(this.holder, value);
|
||||
await expect(tx)
|
||||
.to.emit(this.underlying, 'Transfer')
|
||||
.withArgs(this.token, this.initialHolder, value)
|
||||
.withArgs(this.token.target, this.holder, value)
|
||||
.to.emit(this.token, 'Transfer')
|
||||
.withArgs(this.initialHolder, ethers.ZeroAddress, value);
|
||||
await expect(tx).to.changeTokenBalances(this.underlying, [this.token, this.initialHolder], [-value, value]);
|
||||
await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, -value);
|
||||
.withArgs(this.holder, ethers.ZeroAddress, value);
|
||||
await expect(tx).to.changeTokenBalances(this.underlying, [this.token, this.holder], [-value, value]);
|
||||
await expect(tx).to.changeTokenBalance(this.token, this.holder, -value);
|
||||
});
|
||||
|
||||
it('entire balance', async function () {
|
||||
const tx = await this.token.connect(this.initialHolder).withdrawTo(this.initialHolder, initialSupply);
|
||||
const tx = await this.token.connect(this.holder).withdrawTo(this.holder, initialSupply);
|
||||
await expect(tx)
|
||||
.to.emit(this.underlying, 'Transfer')
|
||||
.withArgs(this.token, this.initialHolder, initialSupply)
|
||||
.withArgs(this.token.target, this.holder, initialSupply)
|
||||
.to.emit(this.token, 'Transfer')
|
||||
.withArgs(this.initialHolder, ethers.ZeroAddress, initialSupply);
|
||||
.withArgs(this.holder, ethers.ZeroAddress, initialSupply);
|
||||
await expect(tx).to.changeTokenBalances(
|
||||
this.underlying,
|
||||
[this.token, this.initialHolder],
|
||||
[this.token, this.holder],
|
||||
[-initialSupply, initialSupply],
|
||||
);
|
||||
await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, -initialSupply);
|
||||
await expect(tx).to.changeTokenBalance(this.token, this.holder, -initialSupply);
|
||||
});
|
||||
|
||||
it('to other account', async function () {
|
||||
const tx = await this.token.connect(this.initialHolder).withdrawTo(this.recipient, initialSupply);
|
||||
const tx = await this.token.connect(this.holder).withdrawTo(this.recipient, initialSupply);
|
||||
await expect(tx)
|
||||
.to.emit(this.underlying, 'Transfer')
|
||||
.withArgs(this.token, this.recipient, initialSupply)
|
||||
.to.emit(this.token, 'Transfer')
|
||||
.withArgs(this.initialHolder, ethers.ZeroAddress, initialSupply);
|
||||
.withArgs(this.holder, ethers.ZeroAddress, initialSupply);
|
||||
await expect(tx).to.changeTokenBalances(
|
||||
this.underlying,
|
||||
[this.token, this.initialHolder, this.recipient],
|
||||
[this.token, this.holder, this.recipient],
|
||||
[-initialSupply, 0, initialSupply],
|
||||
);
|
||||
await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, -initialSupply);
|
||||
await expect(tx).to.changeTokenBalance(this.token, this.holder, -initialSupply);
|
||||
});
|
||||
|
||||
it('reverts withdrawing to the wrapper contract', async function () {
|
||||
await expect(this.token.connect(this.initialHolder).withdrawTo(this.token, initialSupply))
|
||||
await expect(this.token.connect(this.holder).withdrawTo(this.token, initialSupply))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver')
|
||||
.withArgs(this.token);
|
||||
});
|
||||
@ -173,8 +175,8 @@ describe('ERC20Wrapper', function () {
|
||||
|
||||
describe('recover', function () {
|
||||
it('nothing to recover', async function () {
|
||||
await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply);
|
||||
await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply);
|
||||
await this.underlying.connect(this.holder).approve(this.token, initialSupply);
|
||||
await this.token.connect(this.holder).depositFor(this.holder, initialSupply);
|
||||
|
||||
const tx = await this.token.$_recover(this.recipient);
|
||||
await expect(tx).to.emit(this.token, 'Transfer').withArgs(ethers.ZeroAddress, this.recipient, 0n);
|
||||
@ -182,7 +184,7 @@ describe('ERC20Wrapper', function () {
|
||||
});
|
||||
|
||||
it('something to recover', async function () {
|
||||
await this.underlying.connect(this.initialHolder).transfer(this.token, initialSupply);
|
||||
await this.underlying.connect(this.holder).transfer(this.token, initialSupply);
|
||||
|
||||
const tx = await this.token.$_recover(this.recipient);
|
||||
await expect(tx).to.emit(this.token, 'Transfer').withArgs(ethers.ZeroAddress, this.recipient, initialSupply);
|
||||
@ -192,8 +194,8 @@ describe('ERC20Wrapper', function () {
|
||||
|
||||
describe('erc20 behaviour', function () {
|
||||
beforeEach(async function () {
|
||||
await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply);
|
||||
await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply);
|
||||
await this.underlying.connect(this.holder).approve(this.token, initialSupply);
|
||||
await this.token.connect(this.holder).depositFor(this.holder, initialSupply);
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC20(initialSupply);
|
||||
|
||||
@ -4,26 +4,43 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const name = 'ERC20Mock';
|
||||
const symbol = 'ERC20Mock';
|
||||
const value = 100n;
|
||||
const data = '0x12345678';
|
||||
|
||||
async function fixture() {
|
||||
const [hasNoCode, owner, receiver, spender] = await ethers.getSigners();
|
||||
const [hasNoCode, owner, receiver, spender, other] = await ethers.getSigners();
|
||||
|
||||
const mock = await ethers.deployContract('$SafeERC20');
|
||||
const erc20ReturnFalseMock = await ethers.deployContract('$ERC20ReturnFalseMock', [name, symbol]);
|
||||
const erc20ReturnTrueMock = await ethers.deployContract('$ERC20', [name, symbol]); // default implementation returns true
|
||||
const erc20NoReturnMock = await ethers.deployContract('$ERC20NoReturnMock', [name, symbol]);
|
||||
const erc20ForceApproveMock = await ethers.deployContract('$ERC20ForceApproveMock', [name, symbol]);
|
||||
const erc1363Mock = await ethers.deployContract('$ERC1363', [name, symbol]);
|
||||
const erc1363ReturnFalseOnErc20Mock = await ethers.deployContract('$ERC1363ReturnFalseOnERC20Mock', [name, symbol]);
|
||||
const erc1363ReturnFalseMock = await ethers.deployContract('$ERC1363ReturnFalseMock', [name, symbol]);
|
||||
const erc1363NoReturnMock = await ethers.deployContract('$ERC1363NoReturnMock', [name, symbol]);
|
||||
const erc1363ForceApproveMock = await ethers.deployContract('$ERC1363ForceApproveMock', [name, symbol]);
|
||||
const erc1363Receiver = await ethers.deployContract('$ERC1363ReceiverMock');
|
||||
const erc1363Spender = await ethers.deployContract('$ERC1363SpenderMock');
|
||||
|
||||
return {
|
||||
hasNoCode,
|
||||
owner,
|
||||
receiver,
|
||||
spender,
|
||||
other,
|
||||
mock,
|
||||
erc20ReturnFalseMock,
|
||||
erc20ReturnTrueMock,
|
||||
erc20NoReturnMock,
|
||||
erc20ForceApproveMock,
|
||||
erc1363Mock,
|
||||
erc1363ReturnFalseOnErc20Mock,
|
||||
erc1363ReturnFalseMock,
|
||||
erc1363NoReturnMock,
|
||||
erc1363ForceApproveMock,
|
||||
erc1363Receiver,
|
||||
erc1363Spender,
|
||||
};
|
||||
}
|
||||
|
||||
@ -118,7 +135,7 @@ describe('SafeERC20', function () {
|
||||
shouldOnlyRevertOnErrors();
|
||||
});
|
||||
|
||||
describe('with usdt approval beaviour', function () {
|
||||
describe('with usdt approval behaviour', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = this.erc20ForceApproveMock;
|
||||
});
|
||||
@ -144,6 +161,186 @@ describe('SafeERC20', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with standard ERC1363', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = this.erc1363Mock;
|
||||
});
|
||||
|
||||
shouldOnlyRevertOnErrors();
|
||||
|
||||
describe('transferAndCall', function () {
|
||||
it('cannot transferAndCall to an EOA directly', async function () {
|
||||
await this.token.$_mint(this.owner, 100n);
|
||||
|
||||
await expect(this.token.connect(this.owner).transferAndCall(this.receiver, value, ethers.Typed.bytes(data)))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver')
|
||||
.withArgs(this.receiver);
|
||||
});
|
||||
|
||||
it('can transferAndCall to an EOA using helper', async function () {
|
||||
await this.token.$_mint(this.mock, value);
|
||||
|
||||
await expect(this.mock.$transferAndCallRelaxed(this.token, this.receiver, value, data))
|
||||
.to.emit(this.token, 'Transfer')
|
||||
.withArgs(this.mock, this.receiver, value);
|
||||
});
|
||||
|
||||
it('can transferAndCall to an ERC1363Receiver using helper', async function () {
|
||||
await this.token.$_mint(this.mock, value);
|
||||
|
||||
await expect(this.mock.$transferAndCallRelaxed(this.token, this.erc1363Receiver, value, data))
|
||||
.to.emit(this.token, 'Transfer')
|
||||
.withArgs(this.mock, this.erc1363Receiver, value)
|
||||
.to.emit(this.erc1363Receiver, 'Received')
|
||||
.withArgs(this.mock, this.mock, value, data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transferFromAndCall', function () {
|
||||
it('can transferFromAndCall to an EOA using helper', async function () {
|
||||
await this.token.$_mint(this.owner, value);
|
||||
await this.token.$_approve(this.owner, this.mock, ethers.MaxUint256);
|
||||
|
||||
await expect(this.mock.$transferFromAndCallRelaxed(this.token, this.owner, this.receiver, value, data))
|
||||
.to.emit(this.token, 'Transfer')
|
||||
.withArgs(this.owner, this.receiver, value);
|
||||
});
|
||||
|
||||
it('can transferFromAndCall to an ERC1363Receiver using helper', async function () {
|
||||
await this.token.$_mint(this.owner, value);
|
||||
await this.token.$_approve(this.owner, this.mock, ethers.MaxUint256);
|
||||
|
||||
await expect(this.mock.$transferFromAndCallRelaxed(this.token, this.owner, this.erc1363Receiver, value, data))
|
||||
.to.emit(this.token, 'Transfer')
|
||||
.withArgs(this.owner, this.erc1363Receiver, value)
|
||||
.to.emit(this.erc1363Receiver, 'Received')
|
||||
.withArgs(this.mock, this.owner, value, data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('approveAndCall', function () {
|
||||
it('can approveAndCall to an EOA using helper', async function () {
|
||||
await expect(this.mock.$approveAndCallRelaxed(this.token, this.receiver, value, data))
|
||||
.to.emit(this.token, 'Approval')
|
||||
.withArgs(this.mock, this.receiver, value);
|
||||
});
|
||||
|
||||
it('can approveAndCall to an ERC1363Spender using helper', async function () {
|
||||
await expect(this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, value, data))
|
||||
.to.emit(this.token, 'Approval')
|
||||
.withArgs(this.mock, this.erc1363Spender, value)
|
||||
.to.emit(this.erc1363Spender, 'Approved')
|
||||
.withArgs(this.mock, value, data);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with ERC1363 that returns false on all ERC20 calls', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = this.erc1363ReturnFalseOnErc20Mock;
|
||||
});
|
||||
|
||||
it('reverts on transferAndCallRelaxed', async function () {
|
||||
await expect(this.mock.$transferAndCallRelaxed(this.token, this.erc1363Receiver, 0n, data))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1363TransferFailed')
|
||||
.withArgs(this.erc1363Receiver, 0n);
|
||||
});
|
||||
|
||||
it('reverts on transferFromAndCallRelaxed', async function () {
|
||||
await expect(this.mock.$transferFromAndCallRelaxed(this.token, this.mock, this.erc1363Receiver, 0n, data))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1363TransferFromFailed')
|
||||
.withArgs(this.mock, this.erc1363Receiver, 0n);
|
||||
});
|
||||
|
||||
it('reverts on approveAndCallRelaxed', async function () {
|
||||
await expect(this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, 0n, data))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1363ApproveFailed')
|
||||
.withArgs(this.erc1363Spender, 0n);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with ERC1363 that returns false on all ERC1363 calls', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = this.erc1363ReturnFalseMock;
|
||||
});
|
||||
|
||||
it('reverts on transferAndCallRelaxed', async function () {
|
||||
await expect(this.mock.$transferAndCallRelaxed(this.token, this.erc1363Receiver, 0n, data))
|
||||
.to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
|
||||
.withArgs(this.token);
|
||||
});
|
||||
|
||||
it('reverts on transferFromAndCallRelaxed', async function () {
|
||||
await expect(this.mock.$transferFromAndCallRelaxed(this.token, this.mock, this.erc1363Receiver, 0n, data))
|
||||
.to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
|
||||
.withArgs(this.token);
|
||||
});
|
||||
|
||||
it('reverts on approveAndCallRelaxed', async function () {
|
||||
await expect(this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, 0n, data))
|
||||
.to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
|
||||
.withArgs(this.token);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with ERC1363 that returns no boolean values', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = this.erc1363NoReturnMock;
|
||||
});
|
||||
|
||||
it('reverts on transferAndCallRelaxed', async function () {
|
||||
await expect(
|
||||
this.mock.$transferAndCallRelaxed(this.token, this.erc1363Receiver, 0n, data),
|
||||
).to.be.revertedWithoutReason();
|
||||
});
|
||||
|
||||
it('reverts on transferFromAndCallRelaxed', async function () {
|
||||
await expect(
|
||||
this.mock.$transferFromAndCallRelaxed(this.token, this.mock, this.erc1363Receiver, 0n, data),
|
||||
).to.be.revertedWithoutReason();
|
||||
});
|
||||
|
||||
it('reverts on approveAndCallRelaxed', async function () {
|
||||
await expect(
|
||||
this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, 0n, data),
|
||||
).to.be.revertedWithoutReason();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with ERC1363 with usdt approval behaviour', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = this.erc1363ForceApproveMock;
|
||||
});
|
||||
|
||||
describe('without initial approval', function () {
|
||||
it('approveAndCallRelaxed works when recipient is an EOA', async function () {
|
||||
await this.mock.$approveAndCallRelaxed(this.token, this.spender, 10n, data);
|
||||
expect(await this.token.allowance(this.mock, this.spender)).to.equal(10n);
|
||||
});
|
||||
|
||||
it('approveAndCallRelaxed works when recipient is a contract', async function () {
|
||||
await this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, 10n, data);
|
||||
expect(await this.token.allowance(this.mock, this.erc1363Spender)).to.equal(10n);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with initial approval', function () {
|
||||
it('approveAndCallRelaxed works when recipient is an EOA', async function () {
|
||||
await this.token.$_approve(this.mock, this.spender, 100n);
|
||||
|
||||
await this.mock.$approveAndCallRelaxed(this.token, this.spender, 10n, data);
|
||||
expect(await this.token.allowance(this.mock, this.spender)).to.equal(10n);
|
||||
});
|
||||
|
||||
it('approveAndCallRelaxed reverts when recipient is a contract', async function () {
|
||||
await this.token.$_approve(this.mock, this.erc1363Spender, 100n);
|
||||
await expect(this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, 10n, data)).to.be.revertedWith(
|
||||
'USDT approval failure',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function shouldOnlyRevertOnErrors() {
|
||||
|
||||
@ -31,6 +31,14 @@ const SIGNATURES = {
|
||||
'onERC1155Received(address,address,uint256,uint256,bytes)',
|
||||
'onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)',
|
||||
],
|
||||
ERC1363: [
|
||||
'transferAndCall(address,uint256)',
|
||||
'transferAndCall(address,uint256,bytes)',
|
||||
'transferFromAndCall(address,address,uint256)',
|
||||
'transferFromAndCall(address,address,uint256,bytes)',
|
||||
'approveAndCall(address,uint256)',
|
||||
'approveAndCall(address,uint256,bytes)',
|
||||
],
|
||||
AccessControl: [
|
||||
'hasRole(bytes32,address)',
|
||||
'getRoleAdmin(bytes32)',
|
||||
|
||||
Reference in New Issue
Block a user