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";
|
import {IERC165} from "./IERC165.sol";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dev Interface of an ERC-1363 compliant contract, as defined in the
|
* @title IERC1363
|
||||||
* https://eips.ethereum.org/EIPS/eip-1363[ERC].
|
* @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
|
* Defines an extension interface for ERC-20 tokens that supports executing code on a recipient contract
|
||||||
* code after `transfer` or `transferFrom`, or spender code after `approve`.
|
* 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.
|
* Note: the ERC-165 identifier for this interface is 0xb0202a11.
|
||||||
* 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
|
* @dev Moves a `value` amount of tokens from the caller's account to `to`
|
||||||
* @param to address The address which you want to transfer to
|
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
|
||||||
* @param amount uint256 The amount of tokens to be transferred
|
* @param to The address which you want to transfer to.
|
||||||
* @return true unless throwing
|
* @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
|
* @dev Moves a `value` amount of tokens from the caller's account to `to`
|
||||||
* @param to address The address which you want to transfer to
|
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
|
||||||
* @param amount uint256 The amount of tokens to be transferred
|
* @param to The address which you want to transfer to.
|
||||||
* @param data bytes Additional data with no specified format, sent in call to `to`
|
* @param value The amount of tokens to be transferred.
|
||||||
* @return true unless throwing
|
* @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
|
* @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
|
||||||
* @param from address The address which you want to send tokens from
|
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
|
||||||
* @param to address The address which you want to transfer to
|
* @param from The address which you want to send tokens from.
|
||||||
* @param amount uint256 The amount of tokens to be transferred
|
* @param to The address which you want to transfer to.
|
||||||
* @return true unless throwing
|
* @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
|
* @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
|
||||||
* @param from address The address which you want to send tokens from
|
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
|
||||||
* @param to address The address which you want to transfer to
|
* @param from The address which you want to send tokens from.
|
||||||
* @param amount uint256 The amount of tokens to be transferred
|
* @param to The address which you want to transfer to.
|
||||||
* @param data bytes Additional data with no specified format, sent in call to `to`
|
* @param value The amount of tokens to be transferred.
|
||||||
* @return true unless throwing
|
* @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
|
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
|
||||||
* and then call `onApprovalReceived` on spender.
|
* caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
|
||||||
* @param spender address The address which will spend the funds
|
* @param spender The address which will spend the funds.
|
||||||
* @param amount uint256 The amount of tokens to be spent
|
* @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
|
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
|
||||||
* and then call `onApprovalReceived` on spender.
|
* caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
|
||||||
* @param spender address The address which will spend the funds
|
* @param spender The address which will spend the funds.
|
||||||
* @param amount uint256 The amount of tokens to be spent
|
* @param value The amount of tokens to be spent.
|
||||||
* @param data bytes Additional data with no specified format, sent in call to `spender`
|
* @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;
|
pragma solidity ^0.8.20;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dev Interface for any contract that wants to support {IERC1363-transferAndCall}
|
* @title IERC1363Receiver
|
||||||
* or {IERC1363-transferFromAndCall} from {ERC1363} token contracts.
|
* @dev Interface for any contract that wants to support `transferAndCall` or `transferFromAndCall`
|
||||||
|
* from ERC-1363 token contracts.
|
||||||
*/
|
*/
|
||||||
interface IERC1363Receiver {
|
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 Whenever ERC-1363 tokens are transferred to this contract via `transferAndCall` or `transferFromAndCall`
|
||||||
* @dev Any ERC-1363 smart contract calls this function on the recipient
|
* by `operator` from `from`, this function is called.
|
||||||
* 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
|
* NOTE: To accept the transfer, this must return
|
||||||
* transaction being reverted.
|
* `bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"))`
|
||||||
* Note: the token contract address is always the message sender.
|
* (i.e. 0x88a7ca5c, or its own function selector).
|
||||||
* @param operator address The address which called `transferAndCall` or `transferFromAndCall` function
|
*
|
||||||
* @param from address The address which are token transferred from
|
* @param operator The address which called `transferAndCall` or `transferFromAndCall` function.
|
||||||
* @param amount uint256 The amount of tokens transferred
|
* @param from The address which are tokens transferred from.
|
||||||
* @param data bytes Additional data with no specified format
|
* @param value The amount of tokens transferred.
|
||||||
* @return `bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"))` unless throwing
|
* @param data Additional data with no specified format.
|
||||||
|
* @return `bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"))` if transfer is allowed unless throwing.
|
||||||
*/
|
*/
|
||||||
function onTransferReceived(
|
function onTransferReceived(
|
||||||
address operator,
|
address operator,
|
||||||
address from,
|
address from,
|
||||||
uint256 amount,
|
uint256 value,
|
||||||
bytes memory data
|
bytes calldata data
|
||||||
) external returns (bytes4);
|
) external returns (bytes4);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,26 +4,23 @@
|
|||||||
pragma solidity ^0.8.20;
|
pragma solidity ^0.8.20;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dev Interface for any contract that wants to support {IERC1363-approveAndCall}
|
* @title ERC1363Spender
|
||||||
* from {ERC1363} token contracts.
|
* @dev Interface for any contract that wants to support `approveAndCall`
|
||||||
|
* from ERC-1363 token contracts.
|
||||||
*/
|
*/
|
||||||
interface IERC1363Spender {
|
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 Whenever an ERC-1363 token `owner` approves this contract via `approveAndCall`
|
||||||
* @dev Any ERC-1363 smart contract calls this function on the recipient
|
* to spend their tokens, this function is called.
|
||||||
* after an `approve`. This function MAY throw to revert and reject the
|
*
|
||||||
* approval. Return of other than the magic value MUST result in the
|
* NOTE: To accept the approval, this must return
|
||||||
* transaction being reverted.
|
* `bytes4(keccak256("onApprovalReceived(address,uint256,bytes)"))`
|
||||||
* Note: the token contract address is always the message sender.
|
* (i.e. 0x7b04a2d0, or its own function selector).
|
||||||
* @param owner address The address which called `approveAndCall` function
|
*
|
||||||
* @param amount uint256 The amount of tokens to be spent
|
* @param owner The address which called `approveAndCall` function and previously owned the tokens.
|
||||||
* @param data bytes Additional data with no specified format
|
* @param value The amount of tokens to be spent.
|
||||||
* @return `bytes4(keccak256("onApprovalReceived(address,uint256,bytes)"))`unless throwing
|
* @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).
|
* {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.
|
* {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}.
|
* {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).
|
* {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:
|
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}}
|
{{ERC20FlashMint}}
|
||||||
|
|
||||||
|
{{ERC1363}}
|
||||||
|
|
||||||
{{ERC4626}}
|
{{ERC4626}}
|
||||||
|
|
||||||
== Utilities
|
== 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;
|
pragma solidity ^0.8.20;
|
||||||
|
|
||||||
import {IERC20} from "../IERC20.sol";
|
import {IERC20} from "../IERC20.sol";
|
||||||
import {IERC20Permit} from "../extensions/IERC20Permit.sol";
|
import {IERC1363} from "../../../interfaces/IERC1363.sol";
|
||||||
import {Address} from "../../../utils/Address.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
|
* @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).
|
* 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 = {}) {
|
function shouldBehaveLikeERC20(initialSupply, opts = {}) {
|
||||||
const { forcedApproval } = 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 () {
|
it('total supply: returns the total token value', async function () {
|
||||||
expect(await this.token.totalSupply()).to.equal(initialSupply);
|
expect(await this.token.totalSupply()).to.equal(initialSupply);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('balanceOf', function () {
|
describe('balanceOf', function () {
|
||||||
it('returns zero when the requested account has no tokens', async 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 () {
|
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 recipient is not the zero address', function () {
|
||||||
describe('when the spender has enough allowance', function () {
|
describe('when the spender has enough allowance', function () {
|
||||||
beforeEach(async 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 () {
|
describe('when the token owner has enough balance', function () {
|
||||||
const value = initialSupply;
|
const value = initialSupply;
|
||||||
|
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
this.tx = await this.token
|
this.tx = await this.token.connect(this.recipient).transferFrom(this.holder, this.other, value);
|
||||||
.connect(this.recipient)
|
|
||||||
.transferFrom(this.initialHolder, this.anotherAccount, value);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('transfers the requested value', async function () {
|
it('transfers the requested value', async function () {
|
||||||
await expect(this.tx).to.changeTokenBalances(
|
await expect(this.tx).to.changeTokenBalances(this.token, [this.holder, this.other], [-value, value]);
|
||||||
this.token,
|
|
||||||
[this.initialHolder, this.anotherAccount],
|
|
||||||
[-value, value],
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('decreases the spender allowance', async function () {
|
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 () {
|
it('emits a transfer event', async function () {
|
||||||
await expect(this.tx)
|
await expect(this.tx).to.emit(this.token, 'Transfer').withArgs(this.holder, this.other, value);
|
||||||
.to.emit(this.token, 'Transfer')
|
|
||||||
.withArgs(this.initialHolder, this.anotherAccount, value);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (forcedApproval) {
|
if (forcedApproval) {
|
||||||
@ -66,9 +62,9 @@ function shouldBehaveLikeERC20(initialSupply, opts = {}) {
|
|||||||
await expect(this.tx)
|
await expect(this.tx)
|
||||||
.to.emit(this.token, 'Approval')
|
.to.emit(this.token, 'Approval')
|
||||||
.withArgs(
|
.withArgs(
|
||||||
this.initialHolder.address,
|
this.holder.address,
|
||||||
this.recipient.address,
|
this.recipient.address,
|
||||||
await this.token.allowance(this.initialHolder, this.recipient),
|
await this.token.allowance(this.holder, this.recipient),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -80,12 +76,10 @@ function shouldBehaveLikeERC20(initialSupply, opts = {}) {
|
|||||||
|
|
||||||
it('reverts when the token owner does not have enough balance', async function () {
|
it('reverts when the token owner does not have enough balance', async function () {
|
||||||
const value = initialSupply;
|
const value = initialSupply;
|
||||||
await this.token.connect(this.initialHolder).transfer(this.anotherAccount, 1n);
|
await this.token.connect(this.holder).transfer(this.other, 1n);
|
||||||
await expect(
|
await expect(this.token.connect(this.recipient).transferFrom(this.holder, this.other, value))
|
||||||
this.token.connect(this.recipient).transferFrom(this.initialHolder, this.anotherAccount, value),
|
|
||||||
)
|
|
||||||
.to.revertedWithCustomError(this.token, 'ERC20InsufficientBalance')
|
.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;
|
const allowance = initialSupply - 1n;
|
||||||
|
|
||||||
beforeEach(async function () {
|
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 () {
|
it('reverts when the token owner has enough balance', async function () {
|
||||||
const value = initialSupply;
|
const value = initialSupply;
|
||||||
await expect(
|
await expect(this.token.connect(this.recipient).transferFrom(this.holder, this.other, value))
|
||||||
this.token.connect(this.recipient).transferFrom(this.initialHolder, this.anotherAccount, value),
|
|
||||||
)
|
|
||||||
.to.be.revertedWithCustomError(this.token, 'ERC20InsufficientAllowance')
|
.to.be.revertedWithCustomError(this.token, 'ERC20InsufficientAllowance')
|
||||||
.withArgs(this.recipient, allowance, value);
|
.withArgs(this.recipient, allowance, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reverts when the token owner does not have enough balance', async function () {
|
it('reverts when the token owner does not have enough balance', async function () {
|
||||||
const value = allowance;
|
const value = allowance;
|
||||||
await this.token.connect(this.initialHolder).transfer(this.anotherAccount, 2);
|
await this.token.connect(this.holder).transfer(this.other, 2);
|
||||||
await expect(
|
await expect(this.token.connect(this.recipient).transferFrom(this.holder, this.other, value))
|
||||||
this.token.connect(this.recipient).transferFrom(this.initialHolder, this.anotherAccount, value),
|
|
||||||
)
|
|
||||||
.to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance')
|
.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 () {
|
describe('when the spender has unlimited allowance', function () {
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
await this.token.connect(this.initialHolder).approve(this.recipient, ethers.MaxUint256);
|
await this.token.connect(this.holder).approve(this.recipient, ethers.MaxUint256);
|
||||||
this.tx = await this.token
|
this.tx = await this.token.connect(this.recipient).transferFrom(this.holder, this.other, 1n);
|
||||||
.connect(this.recipient)
|
|
||||||
.transferFrom(this.initialHolder, this.anotherAccount, 1n);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not decrease the spender allowance', async function () {
|
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 () {
|
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 () {
|
it('reverts when the recipient is the zero address', async function () {
|
||||||
const value = initialSupply;
|
const value = initialSupply;
|
||||||
await this.token.connect(this.initialHolder).approve(this.recipient, value);
|
await this.token.connect(this.holder).approve(this.recipient, value);
|
||||||
await expect(this.token.connect(this.recipient).transferFrom(this.initialHolder, ethers.ZeroAddress, value))
|
await expect(this.token.connect(this.recipient).transferFrom(this.holder, ethers.ZeroAddress, value))
|
||||||
.to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver')
|
.to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver')
|
||||||
.withArgs(ethers.ZeroAddress);
|
.withArgs(ethers.ZeroAddress);
|
||||||
});
|
});
|
||||||
@ -164,24 +152,24 @@ function shouldBehaveLikeERC20Transfer(balance) {
|
|||||||
describe('when the recipient is not the zero address', function () {
|
describe('when the recipient is not the zero address', function () {
|
||||||
it('reverts when the sender does not have enough balance', async function () {
|
it('reverts when the sender does not have enough balance', async function () {
|
||||||
const value = balance + 1n;
|
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')
|
.to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance')
|
||||||
.withArgs(this.initialHolder, balance, value);
|
.withArgs(this.holder, balance, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the sender transfers all balance', function () {
|
describe('when the sender transfers all balance', function () {
|
||||||
const value = balance;
|
const value = balance;
|
||||||
|
|
||||||
beforeEach(async function () {
|
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 () {
|
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 () {
|
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;
|
const value = 0n;
|
||||||
|
|
||||||
beforeEach(async function () {
|
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 () {
|
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 () {
|
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 () {
|
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')
|
.to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver')
|
||||||
.withArgs(ethers.ZeroAddress);
|
.withArgs(ethers.ZeroAddress);
|
||||||
});
|
});
|
||||||
@ -215,22 +203,22 @@ function shouldBehaveLikeERC20Approve(supply) {
|
|||||||
const value = supply;
|
const value = supply;
|
||||||
|
|
||||||
it('emits an approval event', async function () {
|
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')
|
.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 () {
|
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 () {
|
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.holder, this.recipient, 1n);
|
||||||
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -238,28 +226,28 @@ function shouldBehaveLikeERC20Approve(supply) {
|
|||||||
const value = supply + 1n;
|
const value = supply + 1n;
|
||||||
|
|
||||||
it('emits an approval event', async function () {
|
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')
|
.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 () {
|
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 () {
|
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.holder, this.recipient, 1n);
|
||||||
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('reverts when the spender is the zero address', async function () {
|
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`)
|
.to.be.revertedWithCustomError(this.token, `ERC20InvalidSpender`)
|
||||||
.withArgs(ethers.ZeroAddress);
|
.withArgs(ethers.ZeroAddress);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -19,12 +19,14 @@ describe('ERC20', function () {
|
|||||||
for (const { Token, forcedApproval } of TOKENS) {
|
for (const { Token, forcedApproval } of TOKENS) {
|
||||||
describe(Token, function () {
|
describe(Token, function () {
|
||||||
const fixture = async () => {
|
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]);
|
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 () {
|
beforeEach(async function () {
|
||||||
@ -87,29 +89,27 @@ describe('ERC20', function () {
|
|||||||
|
|
||||||
describe('for a non zero account', function () {
|
describe('for a non zero account', function () {
|
||||||
it('rejects burning more than balance', async 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')
|
.to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance')
|
||||||
.withArgs(this.initialHolder, initialSupply, initialSupply + 1n);
|
.withArgs(this.holder, initialSupply, initialSupply + 1n);
|
||||||
});
|
});
|
||||||
|
|
||||||
const describeBurn = function (description, value) {
|
const describeBurn = function (description, value) {
|
||||||
describe(description, function () {
|
describe(description, function () {
|
||||||
beforeEach('burning', async 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 () {
|
it('decrements totalSupply', async function () {
|
||||||
expect(await this.token.totalSupply()).to.equal(initialSupply - value);
|
expect(await this.token.totalSupply()).to.equal(initialSupply - value);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('decrements initialHolder balance', async function () {
|
it('decrements holder balance', async function () {
|
||||||
await expect(this.tx).to.changeTokenBalance(this.token, this.initialHolder, -value);
|
await expect(this.tx).to.changeTokenBalance(this.token, this.holder, -value);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emits Transfer event', async function () {
|
it('emits Transfer event', async function () {
|
||||||
await expect(this.tx)
|
await expect(this.tx).to.emit(this.token, 'Transfer').withArgs(this.holder, ethers.ZeroAddress, value);
|
||||||
.to.emit(this.token, 'Transfer')
|
|
||||||
.withArgs(this.initialHolder, ethers.ZeroAddress, value);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -127,19 +127,19 @@ describe('ERC20', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('from is the zero address', async function () {
|
it('from is the zero address', async function () {
|
||||||
const tx = await this.token.$_update(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.initialHolder, 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);
|
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 () {
|
it('to is the zero address', async function () {
|
||||||
const tx = await this.token.$_update(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.initialHolder, 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);
|
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 () {
|
describe('from and to are the same address', function () {
|
||||||
@ -159,9 +159,9 @@ describe('ERC20', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('executes with balance', async function () {
|
it('executes with balance', async function () {
|
||||||
const tx = await this.token.$_update(this.initialHolder, this.initialHolder, value);
|
const tx = await this.token.$_update(this.holder, this.holder, value);
|
||||||
await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, 0n);
|
await expect(tx).to.changeTokenBalance(this.token, this.holder, 0n);
|
||||||
await expect(tx).to.emit(this.token, 'Transfer').withArgs(this.initialHolder, this.initialHolder, value);
|
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;
|
const loanValue = 10_000_000_000_000n;
|
||||||
|
|
||||||
async function fixture() {
|
async function fixture() {
|
||||||
const [initialHolder, other, anotherAccount] = await ethers.getSigners();
|
const [holder, other] = await ethers.getSigners();
|
||||||
|
|
||||||
const token = await ethers.deployContract('$ERC20FlashMintMock', [name, symbol]);
|
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 () {
|
describe('ERC20FlashMint', function () {
|
||||||
@ -134,7 +134,7 @@ describe('ERC20FlashMint', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('custom flash fee receiver', async function () {
|
it('custom flash fee receiver', async function () {
|
||||||
const flashFeeReceiverAddress = this.anotherAccount;
|
const flashFeeReceiverAddress = this.other;
|
||||||
await this.token.setFlashFeeReceiver(flashFeeReceiverAddress);
|
await this.token.setFlashFeeReceiver(flashFeeReceiverAddress);
|
||||||
expect(await this.token.$_flashFeeReceiver()).to.equal(flashFeeReceiverAddress);
|
expect(await this.token.$_flashFeeReceiver()).to.equal(flashFeeReceiverAddress);
|
||||||
|
|
||||||
|
|||||||
@ -10,13 +10,13 @@ const symbol = 'MTKN';
|
|||||||
const initialSupply = 100n;
|
const initialSupply = 100n;
|
||||||
|
|
||||||
async function fixture() {
|
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]);
|
const token = await ethers.deployContract('$ERC20Permit', [name, symbol, name]);
|
||||||
await token.$_mint(initialHolder, initialSupply);
|
await token.$_mint(holder, initialSupply);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initialHolder,
|
holder,
|
||||||
spender,
|
spender,
|
||||||
owner,
|
owner,
|
||||||
other,
|
other,
|
||||||
@ -30,7 +30,7 @@ describe('ERC20Permit', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('initial nonce is 0', async 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 () {
|
it('domain separator', async function () {
|
||||||
|
|||||||
@ -10,14 +10,16 @@ const decimals = 9n;
|
|||||||
const initialSupply = 100n;
|
const initialSupply = 100n;
|
||||||
|
|
||||||
async function fixture() {
|
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]);
|
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]);
|
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 () {
|
describe('ERC20Wrapper', function () {
|
||||||
@ -53,57 +55,57 @@ describe('ERC20Wrapper', function () {
|
|||||||
|
|
||||||
describe('deposit', function () {
|
describe('deposit', function () {
|
||||||
it('executes with approval', async 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)
|
await expect(tx)
|
||||||
.to.emit(this.underlying, 'Transfer')
|
.to.emit(this.underlying, 'Transfer')
|
||||||
.withArgs(this.initialHolder, this.token, initialSupply)
|
.withArgs(this.holder, this.token, initialSupply)
|
||||||
.to.emit(this.token, 'Transfer')
|
.to.emit(this.token, 'Transfer')
|
||||||
.withArgs(ethers.ZeroAddress, this.initialHolder, initialSupply);
|
.withArgs(ethers.ZeroAddress, this.holder, initialSupply);
|
||||||
await expect(tx).to.changeTokenBalances(
|
await expect(tx).to.changeTokenBalances(
|
||||||
this.underlying,
|
this.underlying,
|
||||||
[this.initialHolder, this.token],
|
[this.holder, this.token],
|
||||||
[-initialSupply, initialSupply],
|
[-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 () {
|
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')
|
.to.be.revertedWithCustomError(this.underlying, 'ERC20InsufficientAllowance')
|
||||||
.withArgs(this.token, 0, initialSupply);
|
.withArgs(this.token, 0, initialSupply);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reverts when inssuficient balance', async function () {
|
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')
|
.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 () {
|
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)
|
await expect(tx)
|
||||||
.to.emit(this.underlying, 'Transfer')
|
.to.emit(this.underlying, 'Transfer')
|
||||||
.withArgs(this.initialHolder, this.token, initialSupply)
|
.withArgs(this.holder, this.token.target, initialSupply)
|
||||||
.to.emit(this.token, 'Transfer')
|
.to.emit(this.token, 'Transfer')
|
||||||
.withArgs(ethers.ZeroAddress, this.recipient, initialSupply);
|
.withArgs(ethers.ZeroAddress, this.recipient, initialSupply);
|
||||||
await expect(tx).to.changeTokenBalances(
|
await expect(tx).to.changeTokenBalances(
|
||||||
this.underlying,
|
this.underlying,
|
||||||
[this.initialHolder, this.token],
|
[this.holder, this.token],
|
||||||
[-initialSupply, initialSupply],
|
[-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 () {
|
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')
|
.to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver')
|
||||||
.withArgs(this.token);
|
.withArgs(this.token);
|
||||||
});
|
});
|
||||||
@ -111,61 +113,61 @@ describe('ERC20Wrapper', function () {
|
|||||||
|
|
||||||
describe('withdraw', function () {
|
describe('withdraw', function () {
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply);
|
await this.underlying.connect(this.holder).approve(this.token, initialSupply);
|
||||||
await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply);
|
await this.token.connect(this.holder).depositFor(this.holder, initialSupply);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reverts when inssuficient balance', async function () {
|
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')
|
.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 () {
|
it('executes when operation is valid', async function () {
|
||||||
const value = 42n;
|
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)
|
await expect(tx)
|
||||||
.to.emit(this.underlying, 'Transfer')
|
.to.emit(this.underlying, 'Transfer')
|
||||||
.withArgs(this.token, this.initialHolder, value)
|
.withArgs(this.token.target, this.holder, value)
|
||||||
.to.emit(this.token, 'Transfer')
|
.to.emit(this.token, 'Transfer')
|
||||||
.withArgs(this.initialHolder, ethers.ZeroAddress, value);
|
.withArgs(this.holder, ethers.ZeroAddress, value);
|
||||||
await expect(tx).to.changeTokenBalances(this.underlying, [this.token, this.initialHolder], [-value, value]);
|
await expect(tx).to.changeTokenBalances(this.underlying, [this.token, this.holder], [-value, value]);
|
||||||
await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, -value);
|
await expect(tx).to.changeTokenBalance(this.token, this.holder, -value);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('entire balance', async function () {
|
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)
|
await expect(tx)
|
||||||
.to.emit(this.underlying, 'Transfer')
|
.to.emit(this.underlying, 'Transfer')
|
||||||
.withArgs(this.token, this.initialHolder, initialSupply)
|
.withArgs(this.token.target, this.holder, initialSupply)
|
||||||
.to.emit(this.token, 'Transfer')
|
.to.emit(this.token, 'Transfer')
|
||||||
.withArgs(this.initialHolder, ethers.ZeroAddress, initialSupply);
|
.withArgs(this.holder, ethers.ZeroAddress, initialSupply);
|
||||||
await expect(tx).to.changeTokenBalances(
|
await expect(tx).to.changeTokenBalances(
|
||||||
this.underlying,
|
this.underlying,
|
||||||
[this.token, this.initialHolder],
|
[this.token, this.holder],
|
||||||
[-initialSupply, initialSupply],
|
[-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 () {
|
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)
|
await expect(tx)
|
||||||
.to.emit(this.underlying, 'Transfer')
|
.to.emit(this.underlying, 'Transfer')
|
||||||
.withArgs(this.token, this.recipient, initialSupply)
|
.withArgs(this.token, this.recipient, initialSupply)
|
||||||
.to.emit(this.token, 'Transfer')
|
.to.emit(this.token, 'Transfer')
|
||||||
.withArgs(this.initialHolder, ethers.ZeroAddress, initialSupply);
|
.withArgs(this.holder, ethers.ZeroAddress, initialSupply);
|
||||||
await expect(tx).to.changeTokenBalances(
|
await expect(tx).to.changeTokenBalances(
|
||||||
this.underlying,
|
this.underlying,
|
||||||
[this.token, this.initialHolder, this.recipient],
|
[this.token, this.holder, this.recipient],
|
||||||
[-initialSupply, 0, initialSupply],
|
[-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 () {
|
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')
|
.to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver')
|
||||||
.withArgs(this.token);
|
.withArgs(this.token);
|
||||||
});
|
});
|
||||||
@ -173,8 +175,8 @@ describe('ERC20Wrapper', function () {
|
|||||||
|
|
||||||
describe('recover', function () {
|
describe('recover', function () {
|
||||||
it('nothing to recover', async function () {
|
it('nothing to recover', async function () {
|
||||||
await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply);
|
await this.underlying.connect(this.holder).approve(this.token, initialSupply);
|
||||||
await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply);
|
await this.token.connect(this.holder).depositFor(this.holder, initialSupply);
|
||||||
|
|
||||||
const tx = await this.token.$_recover(this.recipient);
|
const tx = await this.token.$_recover(this.recipient);
|
||||||
await expect(tx).to.emit(this.token, 'Transfer').withArgs(ethers.ZeroAddress, this.recipient, 0n);
|
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 () {
|
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);
|
const tx = await this.token.$_recover(this.recipient);
|
||||||
await expect(tx).to.emit(this.token, 'Transfer').withArgs(ethers.ZeroAddress, this.recipient, initialSupply);
|
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 () {
|
describe('erc20 behaviour', function () {
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply);
|
await this.underlying.connect(this.holder).approve(this.token, initialSupply);
|
||||||
await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply);
|
await this.token.connect(this.holder).depositFor(this.holder, initialSupply);
|
||||||
});
|
});
|
||||||
|
|
||||||
shouldBehaveLikeERC20(initialSupply);
|
shouldBehaveLikeERC20(initialSupply);
|
||||||
|
|||||||
@ -4,26 +4,43 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
|||||||
|
|
||||||
const name = 'ERC20Mock';
|
const name = 'ERC20Mock';
|
||||||
const symbol = 'ERC20Mock';
|
const symbol = 'ERC20Mock';
|
||||||
|
const value = 100n;
|
||||||
|
const data = '0x12345678';
|
||||||
|
|
||||||
async function fixture() {
|
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 mock = await ethers.deployContract('$SafeERC20');
|
||||||
const erc20ReturnFalseMock = await ethers.deployContract('$ERC20ReturnFalseMock', [name, symbol]);
|
const erc20ReturnFalseMock = await ethers.deployContract('$ERC20ReturnFalseMock', [name, symbol]);
|
||||||
const erc20ReturnTrueMock = await ethers.deployContract('$ERC20', [name, symbol]); // default implementation returns true
|
const erc20ReturnTrueMock = await ethers.deployContract('$ERC20', [name, symbol]); // default implementation returns true
|
||||||
const erc20NoReturnMock = await ethers.deployContract('$ERC20NoReturnMock', [name, symbol]);
|
const erc20NoReturnMock = await ethers.deployContract('$ERC20NoReturnMock', [name, symbol]);
|
||||||
const erc20ForceApproveMock = await ethers.deployContract('$ERC20ForceApproveMock', [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 {
|
return {
|
||||||
hasNoCode,
|
hasNoCode,
|
||||||
owner,
|
owner,
|
||||||
receiver,
|
receiver,
|
||||||
spender,
|
spender,
|
||||||
|
other,
|
||||||
mock,
|
mock,
|
||||||
erc20ReturnFalseMock,
|
erc20ReturnFalseMock,
|
||||||
erc20ReturnTrueMock,
|
erc20ReturnTrueMock,
|
||||||
erc20NoReturnMock,
|
erc20NoReturnMock,
|
||||||
erc20ForceApproveMock,
|
erc20ForceApproveMock,
|
||||||
|
erc1363Mock,
|
||||||
|
erc1363ReturnFalseOnErc20Mock,
|
||||||
|
erc1363ReturnFalseMock,
|
||||||
|
erc1363NoReturnMock,
|
||||||
|
erc1363ForceApproveMock,
|
||||||
|
erc1363Receiver,
|
||||||
|
erc1363Spender,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +135,7 @@ describe('SafeERC20', function () {
|
|||||||
shouldOnlyRevertOnErrors();
|
shouldOnlyRevertOnErrors();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with usdt approval beaviour', function () {
|
describe('with usdt approval behaviour', function () {
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
this.token = this.erc20ForceApproveMock;
|
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() {
|
function shouldOnlyRevertOnErrors() {
|
||||||
|
|||||||
@ -31,6 +31,14 @@ const SIGNATURES = {
|
|||||||
'onERC1155Received(address,address,uint256,uint256,bytes)',
|
'onERC1155Received(address,address,uint256,uint256,bytes)',
|
||||||
'onERC1155BatchReceived(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: [
|
AccessControl: [
|
||||||
'hasRole(bytes32,address)',
|
'hasRole(bytes32,address)',
|
||||||
'getRoleAdmin(bytes32)',
|
'getRoleAdmin(bytes32)',
|
||||||
|
|||||||
Reference in New Issue
Block a user