Skip to content

Commit

Permalink
EIP2612 permit (#618)
Browse files Browse the repository at this point in the history
  • Loading branch information
k1rill-fedoseev committed Aug 6, 2021
1 parent 0d191e1 commit 3519ddb
Show file tree
Hide file tree
Showing 9 changed files with 352 additions and 536 deletions.
2 changes: 1 addition & 1 deletion contracts/ERC677BridgeToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ contract ERC677BridgeToken is IBurnableMintableERC677Token, DetailedERC20, Burna
}

function getTokenInterfacesVersion() external pure returns (uint64 major, uint64 minor, uint64 patch) {
return (2, 4, 0);
return (2, 5, 0);
}

function superTransfer(address _to, uint256 _value) internal returns (bool) {
Expand Down
132 changes: 102 additions & 30 deletions contracts/PermittableToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ contract PermittableToken is ERC677BridgeToken {

// EIP712 niceties
bytes32 public DOMAIN_SEPARATOR;
// bytes32 public constant PERMIT_TYPEHASH = keccak256("Permit(address holder,address spender,uint256 nonce,uint256 expiry,bool allowed)");
bytes32 public constant PERMIT_TYPEHASH = 0xea2aa0a1be11a07ed86d755c93467f4f82362b452371d1ba94d1715123511acb;
// bytes32 public constant PERMIT_TYPEHASH_LEGACY = keccak256("Permit(address holder,address spender,uint256 nonce,uint256 expiry,bool allowed)");
bytes32 public constant PERMIT_TYPEHASH_LEGACY = 0xea2aa0a1be11a07ed86d755c93467f4f82362b452371d1ba94d1715123511acb;
// bytes32 public constant PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;

mapping(address => uint256) public nonces;
mapping(address => mapping(address => uint256)) public expirations;
Expand Down Expand Up @@ -56,7 +58,7 @@ contract PermittableToken is ERC677BridgeToken {
} else {
// If allowance is unlimited by `permit`, `approve`, or `increaseAllowance`
// function, don't adjust it. But the expiration date must be empty or in the future
require(expirations[_sender][msg.sender] == 0 || expirations[_sender][msg.sender] >= _now());
require(expirations[_sender][msg.sender] == 0 || expirations[_sender][msg.sender] >= now);
}
} else {
// If `_sender` is `msg.sender`,
Expand All @@ -71,20 +73,16 @@ contract PermittableToken is ERC677BridgeToken {
/// @param _to The address which will spend the funds.
/// @param _value The amount of tokens to be spent.
function approve(address _to, uint256 _value) public returns (bool result) {
result = super.approve(_to, _value);
if (_value == uint256(-1)) {
delete expirations[msg.sender][_to];
}
_approveAndResetExpirations(msg.sender, _to, _value);
return true;
}

/// @dev Atomically increases the allowance granted to spender by the caller.
/// @param _to The address which will spend the funds.
/// @param _addedValue The amount of tokens to increase the allowance by.
function increaseAllowance(address _to, uint256 _addedValue) public returns (bool result) {
result = super.increaseAllowance(_to, _addedValue);
if (allowed[msg.sender][_to] == uint256(-1)) {
delete expirations[msg.sender][_to];
}
_approveAndResetExpirations(msg.sender, _to, allowed[msg.sender][_to].add(_addedValue));
return true;
}

/// @dev An alias for `transfer` function.
Expand Down Expand Up @@ -134,33 +132,107 @@ contract PermittableToken is ERC677BridgeToken {
bytes32 _r,
bytes32 _s
) external {
require(_holder != address(0));
require(_spender != address(0));
require(_expiry == 0 || _now() <= _expiry);

require(_v == 27 || _v == 28);
require(uint256(_s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0);

bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, _holder, _spender, _nonce, _expiry, _allowed))
)
);
require(_expiry == 0 || now <= _expiry);

require(_holder == ecrecover(digest, _v, _r, _s));
bytes32 digest = _digest(abi.encode(PERMIT_TYPEHASH_LEGACY, _holder, _spender, _nonce, _expiry, _allowed));

require(_holder == _recover(digest, _v, _r, _s));
require(_nonce == nonces[_holder]++);

uint256 amount = _allowed ? uint256(-1) : 0;

allowed[_holder][_spender] = amount;
expirations[_holder][_spender] = _allowed ? _expiry : 0;

emit Approval(_holder, _spender, amount);
_approve(_holder, _spender, amount);
}

function _now() internal view returns (uint256) {
return now;
/** @dev Allows to spend holder's unlimited amount by the specified spender according to EIP2612.
* The function can be called by anyone, but requires having allowance parameters
* signed by the holder according to EIP712.
* @param _holder The holder's address.
* @param _spender The spender's address.
* @param _value Allowance value to set as a result of the call.
* @param _deadline The deadline timestamp to call the permit function. Must be a timestamp in the future.
* Note that timestamps are not precise, malicious miner/validator can manipulate them to some extend.
* Assume that there can be a 900 seconds time delta between the desired timestamp and the actual expiration.
* @param _v A final byte of signature (ECDSA component).
* @param _r The first 32 bytes of signature (ECDSA component).
* @param _s The second 32 bytes of signature (ECDSA component).
*/
function permit(
address _holder,
address _spender,
uint256 _value,
uint256 _deadline,
uint8 _v,
bytes32 _r,
bytes32 _s
) external {
require(now <= _deadline);

uint256 nonce = nonces[_holder]++;
bytes32 digest = _digest(abi.encode(PERMIT_TYPEHASH, _holder, _spender, _value, nonce, _deadline));

require(_holder == _recover(digest, _v, _r, _s));

_approveAndResetExpirations(_holder, _spender, _value);
}

/**
* @dev Sets a new allowance value for the given owner and spender addresses.
* Resets expiration timestamp in case of unlimited approval.
* @param _owner address tokens holder.
* @param _spender address of tokens spender.
* @param _amount amount of approved tokens.
*/
function _approveAndResetExpirations(address _owner, address _spender, uint256 _amount) internal {
_approve(_owner, _spender, _amount);

// it is not necessary to reset _expirations in other cases, since it is only used together with infinite allowance
if (_amount == uint256(-1)) {
delete expirations[_owner][_spender];
}
}

/**
* @dev Internal function for issuing an allowance.
* @param _owner address of the tokens owner.
* @param _spender address of the approved tokens spender.
* @param _amount amount of the approved tokens.
*/
function _approve(address _owner, address _spender, uint256 _amount) internal {
require(_owner != address(0), "ERC20: approve from the zero address");
require(_spender != address(0), "ERC20: approve to the zero address");

allowed[_owner][_spender] = _amount;
emit Approval(_owner, _spender, _amount);
}

/**
* @dev Calculates the message digest for encoded EIP712 typed struct.
* @param _typedStruct encoded payload.
*/
function _digest(bytes memory _typedStruct) internal view returns (bytes32) {
return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, keccak256(_typedStruct)));
}

/**
* @dev Derives the signer address for the given message digest and ECDSA signature params.
* @param _digest signed message digest.
* @param _v a final byte of signature (ECDSA component).
* @param _r the first 32 bytes of the signature (ECDSA component).
* @param _s the second 32 bytes of the signature (ECDSA component).
*/
function _recover(bytes32 _digest, uint8 _v, bytes32 _r, bytes32 _s) internal pure returns (address) {
require(_v == 27 || _v == 28, "ECDSA: invalid signature 'v' value");
require(
uint256(_s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0,
"ECDSA: invalid signature 's' value"
);

address signer = ecrecover(_digest, _v, _r, _s);
require(signer != address(0), "ECDSA: invalid signature");

return signer;
}
}
9 changes: 0 additions & 9 deletions contracts/mocks/ERC677BridgeTokenRewardableMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,4 @@ contract ERC677BridgeTokenRewardableMock is ERC677BridgeTokenRewardable {
function setStakingContractMock(address _stakingContract) public {
stakingContract = _stakingContract;
}

function setNow(uint256 _timestamp) public {
_blockTimestamp = _timestamp;
}

function _now() internal view returns (uint256) {
return _blockTimestamp != 0 ? _blockTimestamp : now;
}

}
23 changes: 0 additions & 23 deletions contracts/mocks/PermittableTokenMock.sol

This file was deleted.

26 changes: 0 additions & 26 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
"eslint-plugin-node": "^10.0.0",
"eslint-plugin-prettier": "^3.0.1",
"eth-gas-reporter": "^0.2.11",
"ethereumjs-abi": "0.6.8",
"ethereumjs-util": "5.2.0",
"nodemon": "^1.17.3",
"prettier": "^1.18.2",
Expand Down
119 changes: 0 additions & 119 deletions test/helpers/eip712.sign.permit.js

This file was deleted.

Loading

0 comments on commit 3519ddb

Please sign in to comment.