Add ERC20 permit
This commit is contained in:
parent
42779b3a5f
commit
d8adb6a700
|
@ -309,11 +309,9 @@ contract Bridge is BridgeGovernance, ReentrancyGuard {
|
||||||
fee: fee
|
fee: fee
|
||||||
});
|
});
|
||||||
|
|
||||||
bytes memory encoded = encodeTransfer(transfer);
|
|
||||||
|
|
||||||
sequence = wormhole().publishMessage{value: callValue}(
|
sequence = wormhole().publishMessage{value: callValue}(
|
||||||
nonce,
|
nonce,
|
||||||
encoded,
|
encodeTransfer(transfer),
|
||||||
finality()
|
finality()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -333,7 +331,6 @@ contract Bridge is BridgeGovernance, ReentrancyGuard {
|
||||||
uint32 nonce,
|
uint32 nonce,
|
||||||
bytes memory payload
|
bytes memory payload
|
||||||
) internal returns (uint64 sequence) {
|
) internal returns (uint64 sequence) {
|
||||||
|
|
||||||
BridgeStructs.TransferWithPayload memory transfer = BridgeStructs
|
BridgeStructs.TransferWithPayload memory transfer = BridgeStructs
|
||||||
.TransferWithPayload({
|
.TransferWithPayload({
|
||||||
payloadID: 3,
|
payloadID: 3,
|
||||||
|
@ -346,14 +343,13 @@ contract Bridge is BridgeGovernance, ReentrancyGuard {
|
||||||
payload: payload
|
payload: payload
|
||||||
});
|
});
|
||||||
|
|
||||||
bytes memory encoded = encodeTransferWithPayload(transfer);
|
|
||||||
|
|
||||||
sequence = wormhole().publishMessage{value: callValue}(
|
sequence = wormhole().publishMessage{value: callValue}(
|
||||||
nonce,
|
nonce,
|
||||||
encoded,
|
encodeTransferWithPayload(transfer),
|
||||||
finality()
|
finality()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateWrapped(bytes memory encodedVm) external returns (address token) {
|
function updateWrapped(bytes memory encodedVm) external returns (address token) {
|
||||||
(IWormhole.VM memory vm, bool valid, string memory reason) = wormhole().parseAndVerifyVM(encodedVm);
|
(IWormhole.VM memory vm, bool valid, string memory reason) = wormhole().parseAndVerifyVM(encodedVm);
|
||||||
|
|
||||||
|
@ -578,11 +574,7 @@ contract Bridge is BridgeGovernance, ReentrancyGuard {
|
||||||
|
|
||||||
function verifyBridgeVM(IWormhole.VM memory vm) internal view returns (bool){
|
function verifyBridgeVM(IWormhole.VM memory vm) internal view returns (bool){
|
||||||
require(!isFork(), "invalid fork");
|
require(!isFork(), "invalid fork");
|
||||||
if (bridgeContracts(vm.emitterChainId) == vm.emitterAddress) {
|
return bridgeContracts(vm.emitterChainId) == vm.emitterAddress;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function encodeAssetMeta(BridgeStructs.AssetMeta memory meta) public pure returns (bytes memory encoded) {
|
function encodeAssetMeta(BridgeStructs.AssetMeta memory meta) public pure returns (bytes memory encoded) {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import "./TokenState.sol";
|
||||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||||
import "@openzeppelin/contracts/utils/Context.sol";
|
import "@openzeppelin/contracts/utils/Context.sol";
|
||||||
import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
|
import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
|
||||||
|
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
|
||||||
|
|
||||||
// Based on the OpenZepplin ERC20 implementation, licensed under MIT
|
// Based on the OpenZepplin ERC20 implementation, licensed under MIT
|
||||||
contract TokenImplementation is TokenState, Context {
|
contract TokenImplementation is TokenState, Context {
|
||||||
|
@ -18,12 +19,33 @@ contract TokenImplementation is TokenState, Context {
|
||||||
string memory symbol_,
|
string memory symbol_,
|
||||||
uint8 decimals_,
|
uint8 decimals_,
|
||||||
uint64 sequence_,
|
uint64 sequence_,
|
||||||
|
|
||||||
address owner_,
|
address owner_,
|
||||||
|
|
||||||
uint16 chainId_,
|
uint16 chainId_,
|
||||||
bytes32 nativeContract_
|
bytes32 nativeContract_
|
||||||
) initializer public {
|
) initializer public {
|
||||||
|
_initializeNativeToken(
|
||||||
|
name_,
|
||||||
|
symbol_,
|
||||||
|
decimals_,
|
||||||
|
sequence_,
|
||||||
|
owner_,
|
||||||
|
chainId_,
|
||||||
|
nativeContract_
|
||||||
|
);
|
||||||
|
|
||||||
|
// initialize w/ EIP712 state variables for domain separator
|
||||||
|
_initializePermitStateIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _initializeNativeToken(
|
||||||
|
string memory name_,
|
||||||
|
string memory symbol_,
|
||||||
|
uint8 decimals_,
|
||||||
|
uint64 sequence_,
|
||||||
|
address owner_,
|
||||||
|
uint16 chainId_,
|
||||||
|
bytes32 nativeContract_
|
||||||
|
) internal {
|
||||||
_state.name = name_;
|
_state.name = name_;
|
||||||
_state.symbol = symbol_;
|
_state.symbol = symbol_;
|
||||||
_state.decimals = decimals_;
|
_state.decimals = decimals_;
|
||||||
|
@ -35,6 +57,21 @@ contract TokenImplementation is TokenState, Context {
|
||||||
_state.nativeContract = nativeContract_;
|
_state.nativeContract = nativeContract_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _initializePermitStateIfNeeded() internal {
|
||||||
|
if (!permitInitialized()) {
|
||||||
|
_state.hashedTokenChain = _hashedTokenChain();
|
||||||
|
_state.hashedNativeContract = _hashedNativeContract();
|
||||||
|
_state.hashedVersion = _hashedDomainVersion();
|
||||||
|
_state.typeHash = _hashedDomainType();
|
||||||
|
_state.cachedChainId = block.chainid;
|
||||||
|
_state.cachedDomainSeparator = _buildNativeDomainSeparator(
|
||||||
|
_state.typeHash, _state.hashedTokenChain, _state.hashedNativeContract, _state.hashedVersion
|
||||||
|
);
|
||||||
|
_state.cachedThis = address(this);
|
||||||
|
_state.permitInitialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function name() public view returns (string memory) {
|
function name() public view returns (string memory) {
|
||||||
return string(abi.encodePacked(_state.name));
|
return string(abi.encodePacked(_state.name));
|
||||||
}
|
}
|
||||||
|
@ -174,4 +211,108 @@ contract TokenImplementation is TokenState, Context {
|
||||||
|
|
||||||
_;
|
_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Returns the domain separator for the current chain.
|
||||||
|
*/
|
||||||
|
function _domainSeparatorV4() internal view returns (bytes32) {
|
||||||
|
if (address(this) == _state.cachedThis && block.chainid == _state.cachedChainId) {
|
||||||
|
return _state.cachedDomainSeparator;
|
||||||
|
} else {
|
||||||
|
return _buildNativeDomainSeparator(
|
||||||
|
_hashedDomainType(),
|
||||||
|
_hashedTokenChain(),
|
||||||
|
_hashedNativeContract(),
|
||||||
|
_hashedDomainVersion()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _buildNativeDomainSeparator(
|
||||||
|
bytes32 typeHash,
|
||||||
|
bytes32 tokenChainHash,
|
||||||
|
bytes32 nativeContractHash,
|
||||||
|
bytes32 versionHash
|
||||||
|
) internal view returns (bytes32) {
|
||||||
|
return keccak256(abi.encode(typeHash, tokenChainHash, nativeContractHash, versionHash, block.chainid, address(this)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this
|
||||||
|
* function returns the hash of the fully encoded EIP712 message for this domain.
|
||||||
|
*
|
||||||
|
* This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example:
|
||||||
|
*
|
||||||
|
* ```solidity
|
||||||
|
* bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
|
||||||
|
* keccak256("Mail(address to,string contents)"),
|
||||||
|
* mailTo,
|
||||||
|
* keccak256(bytes(mailContents))
|
||||||
|
* )));
|
||||||
|
* address signer = ECDSA.recover(digest, signature);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
function _hashTypedDataV4(bytes32 structHash) internal view returns (bytes32) {
|
||||||
|
return ECDSA.toTypedDataHash(_domainSeparatorV4(), structHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev See {IERC20Permit-permit}.
|
||||||
|
*/
|
||||||
|
function permit(
|
||||||
|
address owner_,
|
||||||
|
address spender_,
|
||||||
|
uint256 value_,
|
||||||
|
uint256 deadline_,
|
||||||
|
uint8 v_,
|
||||||
|
bytes32 r_,
|
||||||
|
bytes32 s_
|
||||||
|
) public {
|
||||||
|
// for those tokens that have been initialized before permit, we need to set
|
||||||
|
// the permit state variables if they have not been set before
|
||||||
|
_initializePermitStateIfNeeded();
|
||||||
|
|
||||||
|
// permit is only allowed before the signature's deadline
|
||||||
|
require(block.timestamp <= deadline_, "ERC20Permit: expired deadline");
|
||||||
|
|
||||||
|
bytes32 structHash = keccak256(
|
||||||
|
abi.encode(_hashedPermitType(), owner_, spender_, value_, _useNonce(owner_), deadline_)
|
||||||
|
);
|
||||||
|
|
||||||
|
bytes32 message = _hashTypedDataV4(structHash);
|
||||||
|
address signer = ECDSA.recover(message, v_, r_, s_);
|
||||||
|
|
||||||
|
// if we cannot recover the token owner, signature is invalid
|
||||||
|
require(signer == owner_, "ERC20Permit: invalid signature");
|
||||||
|
|
||||||
|
_approve(owner_, spender_, value_);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev See {IERC20Permit-DOMAIN_SEPARATOR}.
|
||||||
|
*/
|
||||||
|
// solhint-disable-next-line func-name-mixedcase
|
||||||
|
function DOMAIN_SEPARATOR() public view returns (bytes32) {
|
||||||
|
return _domainSeparatorV4();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _hashedTokenChain() internal view returns (bytes32) {
|
||||||
|
return keccak256(abi.encodePacked(_state.chainId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function _hashedNativeContract() internal view returns (bytes32) {
|
||||||
|
return keccak256(abi.encodePacked(_state.nativeContract));
|
||||||
|
}
|
||||||
|
|
||||||
|
function _hashedDomainVersion() internal pure returns (bytes32) {
|
||||||
|
return keccak256(bytes("1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function _hashedDomainType() internal pure returns (bytes32) {
|
||||||
|
return keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
|
||||||
|
}
|
||||||
|
|
||||||
|
function _hashedPermitType() internal pure returns (bytes32) {
|
||||||
|
return keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
pragma solidity ^0.8.0;
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
|
import "@openzeppelin/contracts/utils/Counters.sol";
|
||||||
|
|
||||||
contract TokenStorage {
|
contract TokenStorage {
|
||||||
struct State {
|
struct State {
|
||||||
string name;
|
string name;
|
||||||
|
@ -23,9 +25,47 @@ contract TokenStorage {
|
||||||
|
|
||||||
uint16 chainId;
|
uint16 chainId;
|
||||||
bytes32 nativeContract;
|
bytes32 nativeContract;
|
||||||
|
|
||||||
|
// EIP712
|
||||||
|
// Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to
|
||||||
|
// invalidate the cached domain separator if the chain id changes.
|
||||||
|
bool permitInitialized;
|
||||||
|
bytes32 cachedDomainSeparator;
|
||||||
|
uint256 cachedChainId;
|
||||||
|
address cachedThis;
|
||||||
|
|
||||||
|
bytes32 hashedTokenChain;
|
||||||
|
bytes32 hashedNativeContract;
|
||||||
|
bytes32 hashedVersion;
|
||||||
|
bytes32 typeHash;
|
||||||
|
|
||||||
|
// ERC20Permit draft
|
||||||
|
mapping(address => Counters.Counter) nonces;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
contract TokenState {
|
contract TokenState {
|
||||||
|
using Counters for Counters.Counter;
|
||||||
|
|
||||||
TokenStorage.State _state;
|
TokenStorage.State _state;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev See {IERC20Permit-nonces}.
|
||||||
|
*/
|
||||||
|
function nonces(address owner_) public view returns (uint256) {
|
||||||
|
return _state.nonces[owner_].current();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev "Consume a nonce": return the current value and increment.
|
||||||
|
*/
|
||||||
|
function _useNonce(address owner_) internal returns (uint256 current) {
|
||||||
|
Counters.Counter storage nonce = _state.nonces[owner_];
|
||||||
|
current = nonce.current();
|
||||||
|
nonce.increment();
|
||||||
|
}
|
||||||
|
|
||||||
|
function permitInitialized() public view returns (bool) {
|
||||||
|
return _state.permitInitialized;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,345 @@
|
||||||
|
// SPDX-License-Identifier: Apache 2
|
||||||
|
|
||||||
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
|
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
|
||||||
|
import "../contracts/bridge/token/TokenImplementation.sol";
|
||||||
|
import "forge-std/Test.sol";
|
||||||
|
|
||||||
|
import "forge-std/console.sol";
|
||||||
|
|
||||||
|
contract TestTokenImplementation is TokenImplementation, Test {
|
||||||
|
uint256 constant SECP256K1_CURVE_ORDER =
|
||||||
|
115792089237316195423570985008687907852837564279074904382605163141518161494337;
|
||||||
|
|
||||||
|
struct InitiateParameters {
|
||||||
|
string name;
|
||||||
|
string symbol;
|
||||||
|
uint8 decimals;
|
||||||
|
uint64 sequence;
|
||||||
|
address owner;
|
||||||
|
uint16 chainId;
|
||||||
|
bytes32 nativeContract;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupTestEnvironmentWithInitialize() public {
|
||||||
|
InitiateParameters memory init;
|
||||||
|
init.name = "Valuable Token";
|
||||||
|
init.symbol = "VALU";
|
||||||
|
init.decimals = 8;
|
||||||
|
init.sequence = 1;
|
||||||
|
init.owner = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF;
|
||||||
|
init.chainId = 1;
|
||||||
|
init
|
||||||
|
.nativeContract = 0x1337133713371337133713371337133713371337133713371337133713371337;
|
||||||
|
|
||||||
|
initialize(
|
||||||
|
init.name,
|
||||||
|
init.symbol,
|
||||||
|
init.decimals,
|
||||||
|
init.sequence,
|
||||||
|
init.owner,
|
||||||
|
init.chainId,
|
||||||
|
init.nativeContract
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupTestEnvironmentWithOldInitialize() public {
|
||||||
|
InitiateParameters memory init;
|
||||||
|
init.name = "Old Valuable Token";
|
||||||
|
init.symbol = "OLD";
|
||||||
|
init.decimals = 8;
|
||||||
|
init.sequence = 1;
|
||||||
|
init.owner = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF;
|
||||||
|
init.chainId = 1;
|
||||||
|
init
|
||||||
|
.nativeContract = 0x1337133713371337133713371337133713371337133713371337133713371337;
|
||||||
|
|
||||||
|
_initializeNativeToken(
|
||||||
|
init.name,
|
||||||
|
init.symbol,
|
||||||
|
init.decimals,
|
||||||
|
init.sequence,
|
||||||
|
init.owner,
|
||||||
|
init.chainId,
|
||||||
|
init.nativeContract
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function simulatePermitSignature(
|
||||||
|
bytes32 walletPrivateKey,
|
||||||
|
address spender,
|
||||||
|
uint256 amount,
|
||||||
|
uint256 deadline
|
||||||
|
)
|
||||||
|
public
|
||||||
|
returns (
|
||||||
|
address allower,
|
||||||
|
uint8 v,
|
||||||
|
bytes32 r,
|
||||||
|
bytes32 s
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// prepare signer allowing for tokens to be spent
|
||||||
|
uint256 sk = uint256(walletPrivateKey);
|
||||||
|
allower = vm.addr(sk);
|
||||||
|
|
||||||
|
bytes32 PERMIT_TYPEHASH = keccak256(
|
||||||
|
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
|
||||||
|
);
|
||||||
|
bytes32 structHash = keccak256(
|
||||||
|
abi.encode(
|
||||||
|
PERMIT_TYPEHASH,
|
||||||
|
allower,
|
||||||
|
spender,
|
||||||
|
amount,
|
||||||
|
nonces(allower),
|
||||||
|
deadline
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
bytes32 message = ECDSA.toTypedDataHash(DOMAIN_SEPARATOR(), structHash);
|
||||||
|
(v, r, s) = vm.sign(sk, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testPermit(
|
||||||
|
bytes32 walletPrivateKey,
|
||||||
|
uint256 amount,
|
||||||
|
address spender
|
||||||
|
) public {
|
||||||
|
vm.assume(walletPrivateKey != bytes32(0));
|
||||||
|
vm.assume(uint256(walletPrivateKey) < SECP256K1_CURVE_ORDER);
|
||||||
|
vm.assume(spender != address(0));
|
||||||
|
|
||||||
|
// initialize TokenImplementation
|
||||||
|
setupTestEnvironmentWithInitialize();
|
||||||
|
|
||||||
|
// prepare signer allowing for tokens to be spent
|
||||||
|
uint256 deadline = 10;
|
||||||
|
(
|
||||||
|
address allower,
|
||||||
|
uint8 v,
|
||||||
|
bytes32 r,
|
||||||
|
bytes32 s
|
||||||
|
) = simulatePermitSignature(
|
||||||
|
walletPrivateKey,
|
||||||
|
spender,
|
||||||
|
amount,
|
||||||
|
deadline
|
||||||
|
);
|
||||||
|
|
||||||
|
// get allowance before calling permit
|
||||||
|
uint256 allowanceBefore = allowance(allower, spender);
|
||||||
|
|
||||||
|
// set allowance with permit
|
||||||
|
permit(allower, spender, amount, deadline, v, r, s);
|
||||||
|
uint256 allowanceAfter = allowance(allower, spender);
|
||||||
|
|
||||||
|
require(
|
||||||
|
allowanceAfter - allowanceBefore == amount,
|
||||||
|
"allowance incorrect"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testFailPermitWithSameSignature(
|
||||||
|
bytes32 walletPrivateKey,
|
||||||
|
uint256 amount,
|
||||||
|
address spender
|
||||||
|
) public {
|
||||||
|
vm.assume(walletPrivateKey != bytes32(0));
|
||||||
|
vm.assume(uint256(walletPrivateKey) < SECP256K1_CURVE_ORDER);
|
||||||
|
vm.assume(spender != address(0));
|
||||||
|
|
||||||
|
// initialize TokenImplementation
|
||||||
|
setupTestEnvironmentWithInitialize();
|
||||||
|
|
||||||
|
// prepare signer allowing for tokens to be spent
|
||||||
|
uint256 deadline = 10;
|
||||||
|
(
|
||||||
|
address allower,
|
||||||
|
uint8 v,
|
||||||
|
bytes32 r,
|
||||||
|
bytes32 s
|
||||||
|
) = simulatePermitSignature(
|
||||||
|
walletPrivateKey,
|
||||||
|
spender,
|
||||||
|
amount,
|
||||||
|
deadline
|
||||||
|
);
|
||||||
|
|
||||||
|
// set allowance with permit
|
||||||
|
permit(allower, spender, amount, deadline, v, r, s);
|
||||||
|
|
||||||
|
// try again... you shall not pass
|
||||||
|
// NOTE: using "testFail" instead of "test" because
|
||||||
|
// vm.expectRevert("ERC20Permit: invalid signature") does not work
|
||||||
|
permit(allower, spender, amount, deadline, v, r, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testFailPermitWithBadSignature(
|
||||||
|
bytes32 walletPrivateKey,
|
||||||
|
uint256 amount,
|
||||||
|
address spender
|
||||||
|
) public {
|
||||||
|
vm.assume(walletPrivateKey != bytes32(0));
|
||||||
|
vm.assume(uint256(walletPrivateKey) < SECP256K1_CURVE_ORDER);
|
||||||
|
vm.assume(spender != address(0));
|
||||||
|
|
||||||
|
// initialize TokenImplementation
|
||||||
|
setupTestEnvironmentWithInitialize();
|
||||||
|
|
||||||
|
// avoid overflow for this test
|
||||||
|
uint256 wrongAmount;
|
||||||
|
unchecked {
|
||||||
|
wrongAmount = amount + 1; // amount will never equal
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare signer allowing for tokens to be spent
|
||||||
|
uint256 deadline = 10;
|
||||||
|
(
|
||||||
|
address allower,
|
||||||
|
uint8 v,
|
||||||
|
bytes32 r,
|
||||||
|
bytes32 s
|
||||||
|
) = simulatePermitSignature(
|
||||||
|
walletPrivateKey,
|
||||||
|
spender,
|
||||||
|
wrongAmount,
|
||||||
|
deadline
|
||||||
|
);
|
||||||
|
|
||||||
|
// you shall not pass!
|
||||||
|
// NOTE: using "testFail" instead of "test" because
|
||||||
|
// vm.expectRevert("ERC20Permit: invalid signature") does not work
|
||||||
|
permit(allower, spender, amount, deadline, v, r, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testPermitWithSignatureUsedAfterDeadline(
|
||||||
|
bytes32 walletPrivateKey,
|
||||||
|
uint256 amount,
|
||||||
|
address spender
|
||||||
|
) public {
|
||||||
|
vm.assume(walletPrivateKey != bytes32(0));
|
||||||
|
vm.assume(uint256(walletPrivateKey) < SECP256K1_CURVE_ORDER);
|
||||||
|
vm.assume(spender != address(0));
|
||||||
|
|
||||||
|
// initialize TokenImplementation
|
||||||
|
setupTestEnvironmentWithInitialize();
|
||||||
|
|
||||||
|
// prepare signer allowing for tokens to be spent
|
||||||
|
uint256 deadline = 10;
|
||||||
|
(
|
||||||
|
address allower,
|
||||||
|
uint8 v,
|
||||||
|
bytes32 r,
|
||||||
|
bytes32 s
|
||||||
|
) = simulatePermitSignature(
|
||||||
|
walletPrivateKey,
|
||||||
|
spender,
|
||||||
|
amount,
|
||||||
|
deadline
|
||||||
|
);
|
||||||
|
|
||||||
|
// waited too long
|
||||||
|
vm.warp(deadline + 1);
|
||||||
|
|
||||||
|
// and fail
|
||||||
|
vm.expectRevert("ERC20Permit: expired deadline");
|
||||||
|
permit(allower, spender, amount, deadline, v, r, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testInitializePermitState() public {
|
||||||
|
// initialize TokenImplementation as if it were the old implementation
|
||||||
|
setupTestEnvironmentWithOldInitialize();
|
||||||
|
require(!permitInitialized(), "permit state should not be initialized");
|
||||||
|
|
||||||
|
// explicity call private method
|
||||||
|
_initializePermitStateIfNeeded();
|
||||||
|
require(permitInitialized(), "permit state should be initialized");
|
||||||
|
|
||||||
|
// check permit state variables
|
||||||
|
require(
|
||||||
|
_state.hashedTokenChain ==
|
||||||
|
keccak256(abi.encodePacked(_state.chainId)),
|
||||||
|
"_state.hashedTokenChain != expected"
|
||||||
|
);
|
||||||
|
require(
|
||||||
|
_state.hashedNativeContract ==
|
||||||
|
keccak256(abi.encodePacked(_state.nativeContract)),
|
||||||
|
"_state.hashedNativeContract != expected"
|
||||||
|
);
|
||||||
|
require(
|
||||||
|
_state.hashedVersion == keccak256(bytes("1")),
|
||||||
|
"_state.hashedVersion != expected"
|
||||||
|
);
|
||||||
|
require(
|
||||||
|
_state.typeHash ==
|
||||||
|
keccak256(
|
||||||
|
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
|
||||||
|
),
|
||||||
|
"_state.typeHash != expected"
|
||||||
|
);
|
||||||
|
require(
|
||||||
|
_state.cachedChainId == block.chainid,
|
||||||
|
"_state.cachedChainId != expected"
|
||||||
|
);
|
||||||
|
require(
|
||||||
|
_state.cachedDomainSeparator ==
|
||||||
|
keccak256(
|
||||||
|
abi.encode(
|
||||||
|
_hashedDomainType(),
|
||||||
|
_hashedTokenChain(),
|
||||||
|
_hashedNativeContract(),
|
||||||
|
_hashedDomainVersion(),
|
||||||
|
block.chainid,
|
||||||
|
address(this)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"_state.cachedDomainSeparator != expected"
|
||||||
|
);
|
||||||
|
require(
|
||||||
|
_state.cachedThis == address(this),
|
||||||
|
"_state.cachedThis != expected"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testPermitForPreviouslyDeployedImplementation(
|
||||||
|
bytes32 walletPrivateKey,
|
||||||
|
uint256 amount,
|
||||||
|
address spender
|
||||||
|
) public {
|
||||||
|
vm.assume(walletPrivateKey != bytes32(0));
|
||||||
|
vm.assume(uint256(walletPrivateKey) < SECP256K1_CURVE_ORDER);
|
||||||
|
vm.assume(spender != address(0));
|
||||||
|
|
||||||
|
// initialize TokenImplementation as if it were the old implementation
|
||||||
|
setupTestEnvironmentWithOldInitialize();
|
||||||
|
|
||||||
|
// prepare signer allowing for tokens to be spent
|
||||||
|
uint256 deadline = 10;
|
||||||
|
(
|
||||||
|
address allower,
|
||||||
|
uint8 v,
|
||||||
|
bytes32 r,
|
||||||
|
bytes32 s
|
||||||
|
) = simulatePermitSignature(
|
||||||
|
walletPrivateKey,
|
||||||
|
spender,
|
||||||
|
amount,
|
||||||
|
deadline
|
||||||
|
);
|
||||||
|
|
||||||
|
// get allowance before calling permit
|
||||||
|
uint256 allowanceBefore = allowance(allower, spender);
|
||||||
|
|
||||||
|
// set allowance with permit
|
||||||
|
permit(allower, spender, amount, deadline, v, r, s);
|
||||||
|
|
||||||
|
uint256 allowanceAfter = allowance(allower, spender);
|
||||||
|
|
||||||
|
require(
|
||||||
|
allowanceAfter - allowanceBefore == amount,
|
||||||
|
"allowance incorrect"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue