Commit a4e82ad7 by github-actions

Transpile e0d2b338

parent 3b23b93a
......@@ -22,6 +22,7 @@
* `BeaconProxy`: added new kind of proxy that allows simultaneous atomic upgrades. ([#2411](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2411))
* `EIP712`: added helpers to verify EIP712 typed data signatures on chain. ([#2418](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2418))
* `ERC20Permit`: added an implementation of the ERC20 permit extension for gasless token approvals. ([#2237](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2237))
* Presets: added token presets with preminted fixed supply `ERC20PresetFixedSupply` and `ERC777PresetFixedSupply`. ([#2399](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2399))
* `Address`: added `functionDelegateCall`, similar to the existing `functionCall`. ([#2333](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2333))
......
......@@ -43,6 +43,14 @@ library ECDSAUpgradeable {
v := byte(0, mload(add(signature, 0x60)))
}
return recover(hash, v, r, s);
}
/**
* @dev Overload of {ECDSA-recover-bytes32-bytes-} that receives the `v`,
* `r` and `s` signature fields separately.
*/
function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {
// EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
// unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
// the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most
......
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.5 <0.8.0;
import "../token/ERC20/ERC20Upgradeable.sol";
import "./IERC20PermitUpgradeable.sol";
import "../cryptography/ECDSAUpgradeable.sol";
import "../utils/CountersUpgradeable.sol";
import "./EIP712Upgradeable.sol";
import "../proxy/Initializable.sol";
/**
* @dev Implementation of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in
* https://eips.ethereum.org/EIPS/eip-2612[EIP-2612].
*
* Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by
* presenting a message signed by the account. By not relying on `{IERC20-approve}`, the token holder account doesn't
* need to send a transaction, and thus is not required to hold Ether at all.
*/
abstract contract ERC20PermitUpgradeable is Initializable, ERC20Upgradeable, IERC20PermitUpgradeable, EIP712Upgradeable {
using CountersUpgradeable for CountersUpgradeable.Counter;
mapping (address => CountersUpgradeable.Counter) private _nonces;
// solhint-disable-next-line var-name-mixedcase
bytes32 private _PERMIT_TYPEHASH;
/**
* @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`.
*
* It's a good idea to use the same `name` that is defined as the ERC20 token name.
*/
function __ERC20Permit_init(string memory name) internal initializer {
__Context_init_unchained();
__EIP712_init_unchained(name, "1");
__ERC20Permit_init_unchained(name);
}
function __ERC20Permit_init_unchained(string memory name) internal initializer {
_PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
}
/**
* @dev See {IERC20Permit-permit}.
*/
function permit(address owner, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public virtual override {
// solhint-disable-next-line not-rely-on-time
require(block.timestamp <= deadline, "ERC20Permit: expired deadline");
bytes32 structHash = keccak256(
abi.encode(
_PERMIT_TYPEHASH,
owner,
spender,
amount,
_nonces[owner].current(),
deadline
)
);
bytes32 hash = _hashTypedDataV4(structHash);
address signer = ECDSAUpgradeable.recover(hash, v, r, s);
require(signer == owner, "ERC20Permit: invalid signature");
_nonces[owner].increment();
_approve(owner, spender, amount);
}
/**
* @dev See {IERC20Permit-nonces}.
*/
function nonces(address owner) public view override returns (uint256) {
return _nonces[owner].current();
}
/**
* @dev See {IERC20Permit-DOMAIN_SEPARATOR}.
*/
// solhint-disable-next-line func-name-mixedcase
function DOMAIN_SEPARATOR() external view override returns (bytes32) {
return _domainSeparatorV4();
}
uint256[49] private __gap;
}
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0 <0.8.0;
/**
* @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in
* https://eips.ethereum.org/EIPS/eip-2612[EIP-2612].
*
* Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by
* presenting a message signed by the account. By not relying on `{IERC20-approve}`, the token holder account doesn't
* need to send a transaction, and thus is not required to hold Ether at all.
*/
interface IERC20PermitUpgradeable {
/**
* @dev Sets `amount` as the allowance of `spender` over `owner`'s tokens,
* given `owner`'s signed approval.
*
* IMPORTANT: The same issues {IERC20-approve} has related to transaction
* ordering also apply here.
*
* Emits an {Approval} event.
*
* Requirements:
*
* - `spender` cannot be the zero address.
* - `deadline` must be a timestamp in the future.
* - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner`
* over the EIP712-formatted function arguments.
* - the signature must use ``owner``'s current nonce (see {nonces}).
*
* For more information on the signature format, see the
* https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP
* section].
*/
function permit(address owner, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external;
/**
* @dev Returns the current nonce for `owner`. This value must be
* included whenever a signature is generated for {permit}.
*
* Every successful call to {permit} increases ``owner``'s nonce by one. This
* prevents a signature from being used multiple times.
*/
function nonces(address owner) external view returns (uint256);
/**
* @dev Returns the domain separator used in the encoding of the signature for `permit`, as defined by {EIP712}.
*/
// solhint-disable-next-line func-name-mixedcase
function DOMAIN_SEPARATOR() external view returns (bytes32);
}
= Draft EIPS
= Draft EIPs
This directory contains implementations of EIPs that are still in Draft status.
......@@ -7,3 +7,9 @@ Due to their nature as drafts, the details of these contracts may change and we
== Cryptography
{{EIP712}}
== ERC 20
{{IERC20Permit}}
{{ERC20Permit}}
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0 <0.8.0;
import "../drafts/ERC20PermitUpgradeable.sol";
import "../proxy/Initializable.sol";
contract ERC20PermitMockUpgradeable is Initializable, ERC20PermitUpgradeable {
function __ERC20PermitMock_init(
string memory name,
string memory symbol,
address initialAccount,
uint256 initialBalance
) internal initializer {
__Context_init_unchained();
__ERC20_init_unchained(name, symbol);
__EIP712_init_unchained(name, "1");
__ERC20Permit_init_unchained(name);
__ERC20PermitMock_init_unchained(name, symbol, initialAccount, initialBalance);
}
function __ERC20PermitMock_init_unchained(
string memory name,
string memory symbol,
address initialAccount,
uint256 initialBalance
) internal initializer {
_mint(initialAccount, initialBalance);
}
function getChainId() external pure returns (uint256 chainId) {
// solhint-disable-next-line no-inline-assembly
assembly {
chainId := chainid()
}
}
uint256[50] private __gap;
}
......@@ -8,6 +8,13 @@ contract TimelockControllerUpgradeableWithInit is TimelockControllerUpgradeable
__TimelockController_init(minDelay, proposers, executors);
}
}
import "../token/ERC20/ERC20Upgradeable.sol";
contract ERC20UpgradeableWithInit is ERC20Upgradeable {
constructor(string memory name_, string memory symbol_) public payable {
__ERC20_init(name_, symbol_);
}
}
import "../GSN/GSNRecipientERC20FeeUpgradeable.sol";
contract GSNRecipientERC20FeeUpgradeableWithInit is GSNRecipientERC20FeeUpgradeable {
......@@ -22,13 +29,6 @@ contract __unstable__ERC20OwnedUpgradeableWithInit is __unstable__ERC20OwnedUpgr
____unstable__ERC20Owned_init(name, symbol);
}
}
import "../token/ERC20/ERC20Upgradeable.sol";
contract ERC20UpgradeableWithInit is ERC20Upgradeable {
constructor(string memory name_, string memory symbol_) public payable {
__ERC20_init(name_, symbol_);
}
}
import "../GSN/GSNRecipientSignatureUpgradeable.sol";
contract GSNRecipientSignatureUpgradeableWithInit is GSNRecipientSignatureUpgradeable {
......@@ -322,6 +322,18 @@ contract ERC20PausableMockUpgradeableWithInit is ERC20PausableMockUpgradeable {
__ERC20PausableMock_init(name, symbol, initialAccount, initialBalance);
}
}
import "./ERC20PermitMockUpgradeable.sol";
contract ERC20PermitMockUpgradeableWithInit is ERC20PermitMockUpgradeable {
constructor(
string memory name,
string memory symbol,
address initialAccount,
uint256 initialBalance
) public payable {
__ERC20PermitMock_init(name, symbol, initialAccount, initialBalance);
}
}
import "./ERC20SnapshotMockUpgradeable.sol";
contract ERC20SnapshotMockUpgradeableWithInit is ERC20SnapshotMockUpgradeable {
......
......@@ -14,6 +14,7 @@ There a few core contracts that implement the behavior specified in the EIP:
Additionally there are multiple custom extensions, including:
* {ERC20Permit}: gasless approval of tokens.
* {ERC20Snapshot}: efficient storage of past token balances to be later queried at any point in time.
* {ERC20Burnable}: destruction of own tokens.
* {ERC20Capped}: enforcement of a cap to the total supply when minting tokens.
......@@ -24,6 +25,11 @@ Finally, there are some utilities to interact with ERC20 contracts in various wa
* {SafeERC20}: a wrapper around the interface that eliminates the need to handle boolean return values.
* {TokenTimelock}: hold tokens for a beneficiary until a specified time.
The following related EIPs are in draft status and can be found in the drafts directory.
- {IERC20Permit}
- {ERC20Permit}
NOTE: This core set of contracts is designed to be unopinionated, allowing developers to access the internal functions in ERC20 (such as <<ERC20-_mint-address-uint256-,`_mint`>>) and expose them as external functions in the way they prefer. On the other hand, xref:ROOT:erc20.adoc#Presets[ERC20 Presets] (such as {ERC20PresetMinterPauser}) are designed using opinionated patterns to provide developers with ready to use, deployable contracts.
== Core
......
......@@ -72,7 +72,8 @@
"rimraf": "^3.0.2",
"solhint": "^3.2.0",
"solidity-coverage": "^0.7.11",
"solidity-docgen": "^0.5.3"
"solidity-docgen": "^0.5.3",
"web3": "^1.3.0"
},
"dependencies": {}
}
const ethSigUtil = require('eth-sig-util');
const Wallet = require('ethereumjs-wallet').default;
const EIP712 = artifacts.require('EIP712External');
const EIP712Domain = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
];
const { EIP712Domain, domainSeparator } = require('../helpers/eip712');
async function domainSeparator (name, version, chainId, verifyingContract) {
return '0x' + ethSigUtil.TypedDataUtils.hashStruct(
'EIP712Domain',
{ name, version, chainId, verifyingContract },
{ EIP712Domain },
).toString('hex');
}
const EIP712 = artifacts.require('EIP712External');
contract('EIP712', function (accounts) {
const [mailTo] = accounts;
......
/* eslint-disable */
const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants;
const { fromRpcSig } = require('ethereumjs-util');
const ethSigUtil = require('eth-sig-util');
const Wallet = require('ethereumjs-wallet').default;
const ERC20PermitMock = artifacts.require('ERC20PermitMock');
const { EIP712Domain, domainSeparator } = require('../helpers/eip712');
const Permit = [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
];
contract('ERC20Permit', function (accounts) {
const [ initialHolder, spender, recipient, other ] = accounts;
const name = 'My Token';
const symbol = 'MTKN';
const version = '1';
const initialSupply = new BN(100);
beforeEach(async function () {
this.token = await ERC20PermitMock.new(name, symbol, initialHolder, initialSupply);
// We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id
// from within the EVM as from the JSON RPC interface.
// See https://github.com/trufflesuite/ganache-core/issues/515
this.chainId = await this.token.getChainId();
});
it('initial nonce is 0', async function () {
expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0');
});
it('domain separator', async function () {
expect(
await this.token.DOMAIN_SEPARATOR(),
).to.equal(
await domainSeparator(name, version, this.chainId, this.token.address),
);
});
describe('permit', function () {
const wallet = Wallet.generate();
const owner = wallet.getAddressString();
const value = new BN(42);
const nonce = 0;
const maxDeadline = MAX_UINT256;
const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({
primaryType: 'Permit',
types: { EIP712Domain, Permit },
domain: { name, version, chainId, verifyingContract },
message: { owner, spender, value, nonce, deadline },
});
it('accepts owner signature', async function () {
const data = buildData(this.chainId, this.token.address);
const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data });
const { v, r, s } = fromRpcSig(signature);
const receipt = await this.token.permit(owner, spender, value, maxDeadline, v, r, s);
expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(value);
});
it('rejects reused signature', async function () {
const data = buildData(this.chainId, this.token.address);
const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data });
const { v, r, s } = fromRpcSig(signature);
await this.token.permit(owner, spender, value, maxDeadline, v, r, s);
await expectRevert(
this.token.permit(owner, spender, value, maxDeadline, v, r, s),
'ERC20Permit: invalid signature',
);
});
it('rejects other signature', async function () {
const otherWallet = Wallet.generate();
const data = buildData(this.chainId, this.token.address);
const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data });
const { v, r, s } = fromRpcSig(signature);
await expectRevert(
this.token.permit(owner, spender, value, maxDeadline, v, r, s),
'ERC20Permit: invalid signature',
);
});
it('rejects expired permit', async function () {
const deadline = (await time.latest()) - time.duration.weeks(1);
const data = buildData(this.chainId, this.token.address, deadline);
const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data });
const { v, r, s } = fromRpcSig(signature);
await expectRevert(
this.token.permit(owner, spender, value, deadline, v, r, s),
'ERC20Permit: expired deadline',
);
});
});
});
const ethSigUtil = require('eth-sig-util');
const EIP712Domain = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
];
async function domainSeparator (name, version, chainId, verifyingContract) {
return '0x' + ethSigUtil.TypedDataUtils.hashStruct(
'EIP712Domain',
{ name, version, chainId, verifyingContract },
{ EIP712Domain },
).toString('hex');
}
module.exports = {
EIP712Domain,
domainSeparator,
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment