Skip to content

Commit 9c4309a

Browse files
committed
implemented permit (eip-2612)
1 parent b30b7be commit 9c4309a

File tree

7 files changed

+170
-22
lines changed

7 files changed

+170
-22
lines changed

packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pragma solidity >= 0.8.11;
33

44
import { ISuperfluidToken } from "./ISuperfluidToken.sol";
55
import { IERC20, IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
6+
import { IERC20Permit } from "@openzeppelin/contracts-v5/token/ERC20/extensions/IERC20Permit.sol";
67
import { IERC777 } from "@openzeppelin/contracts/token/ERC777/IERC777.sol";
78
import { IPoolAdminNFT } from "../agreements/gdav1/IPoolAdminNFT.sol";
89
import { IPoolMemberNFT } from "../agreements/gdav1/IPoolMemberNFT.sol";
@@ -11,25 +12,27 @@ import { IPoolMemberNFT } from "../agreements/gdav1/IPoolMemberNFT.sol";
1112
* @title Super token (Superfluid Token + ERC20 + ERC777) interface
1213
* @author Superfluid
1314
*/
14-
interface ISuperToken is ISuperfluidToken, IERC20Metadata, IERC777 {
15+
interface ISuperToken is ISuperfluidToken, IERC20Metadata, IERC777, IERC20Permit {
1516

1617
/**************************************************************************
1718
* Errors
1819
*************************************************************************/
19-
error SUPER_TOKEN_CALLER_IS_NOT_OPERATOR_FOR_HOLDER(); // 0xf7f02227
20-
error SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT(); // 0xfe737d05
21-
error SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED(); // 0xe3e13698
22-
error SUPER_TOKEN_NO_UNDERLYING_TOKEN(); // 0xf79cf656
23-
error SUPER_TOKEN_ONLY_SELF(); // 0x7ffa6648
24-
error SUPER_TOKEN_ONLY_ADMIN(); // 0x0484acab
25-
error SUPER_TOKEN_ONLY_GOV_OWNER(); // 0xd9c7ed08
26-
error SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS(); // 0x81638627
27-
error SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS(); // 0xdf070274
28-
error SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS(); // 0xba2ab184
29-
error SUPER_TOKEN_MINT_TO_ZERO_ADDRESS(); // 0x0d243157
30-
error SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS(); // 0xeecd6c9b
31-
error SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS(); // 0xe219bd39
32-
error SUPER_TOKEN_NFT_PROXY_ADDRESS_CHANGED(); // 0x6bef249d
20+
error SUPER_TOKEN_CALLER_IS_NOT_OPERATOR_FOR_HOLDER(); // 0xf7f02227
21+
error SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT(); // 0xfe737d05
22+
error SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED(); // 0xe3e13698
23+
error SUPER_TOKEN_NO_UNDERLYING_TOKEN(); // 0xf79cf656
24+
error SUPER_TOKEN_ONLY_SELF(); // 0x7ffa6648
25+
error SUPER_TOKEN_ONLY_ADMIN(); // 0x0484acab
26+
error SUPER_TOKEN_ONLY_GOV_OWNER(); // 0xd9c7ed08
27+
error SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS(); // 0x81638627
28+
error SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS(); // 0xdf070274
29+
error SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS(); // 0xba2ab184
30+
error SUPER_TOKEN_MINT_TO_ZERO_ADDRESS(); // 0x0d243157
31+
error SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS(); // 0xeecd6c9b
32+
error SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS(); // 0xe219bd39
33+
error SUPER_TOKEN_NFT_PROXY_ADDRESS_CHANGED(); // 0xef1b6ddf
34+
error SUPER_TOKEN_PERMIT_EXPIRED_SIGNATURE(uint256 deadline); // 0x6e72b90f
35+
error SUPER_TOKEN_PERMIT_INVALID_SIGNER(address signer, address owner); // 0xb6422105
3336

3437
/**
3538
* @dev Initialize the contract

packages/ethereum-contracts/contracts/mocks/SuperTokenMock.t.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ contract SuperTokenStorageLayoutTester is SuperToken {
6767
require (slot == 18 && offset == 0, "_operators changed location");
6868
// uses 4 slots
6969

70-
assembly { slot:= _reserve22.slot offset := _reserve22.offset }
71-
require (slot == 22 && offset == 0, "_reserve22 changed location");
70+
assembly { slot:= _reserve23.slot offset := _reserve23.offset }
71+
require (slot == 23 && offset == 0, "_reserve23 changed location");
7272

7373
assembly { slot:= _reserve31.slot offset := _reserve31.offset }
7474
require (slot == 31 && offset == 0, "_reserve31 changed location");

packages/ethereum-contracts/contracts/superfluid/SuperToken.sol

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import { IERC777Recipient } from "@openzeppelin/contracts/token/ERC777/IERC777Re
2121
import { IERC777Sender } from "@openzeppelin/contracts/token/ERC777/IERC777Sender.sol";
2222
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
2323

24+
import { ECDSA } from "@openzeppelin/contracts-v5/utils/cryptography/ECDSA.sol";
25+
import { MessageHashUtils } from "@openzeppelin/contracts-v5/utils/cryptography/MessageHashUtils.sol";
26+
2427
// placeholder types needed as an intermediate step before complete removal of FlowNFTs
2528
// solhint-disable-next-line no-empty-blocks
2629
interface IConstantOutflowNFT {}
@@ -37,7 +40,6 @@ contract SuperToken is
3740
SuperfluidToken,
3841
ISuperToken
3942
{
40-
4143
using SafeMath for uint256;
4244
using SafeCast for uint256;
4345
using Address for address;
@@ -49,6 +51,10 @@ contract SuperToken is
4951

5052
uint8 constant private _STANDARD_DECIMALS = 18;
5153

54+
// EIP-712 permit typehash
55+
bytes32 constant private _PERMIT_TYPEHASH =
56+
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
57+
5258
// solhint-disable-next-line var-name-mixedcase
5359
IConstantOutflowNFT immutable public CONSTANT_OUTFLOW_NFT;
5460

@@ -84,15 +90,18 @@ contract SuperToken is
8490
/// @dev ERC777 operators support data
8591
ERC777Helper.Operators internal _operators;
8692

93+
/// @dev ERC20 Nonces for EIP-2612 (permit)
94+
mapping(address account => uint256) internal _nonces;
95+
8796
// NOTE: for future compatibility, these are reserved solidity slots
8897
// The sub-class of SuperToken solidity slot will start after _reserve22
8998

9099
// NOTE: Whenever modifying the storage layout here it is important to update the validateStorageLayout
91100
// function in its respective mock contract to ensure that it doesn't break anything or lead to unexpected
92101
// behaviors/layout when upgrading
93102

94-
uint256 internal _reserve22;
95-
uint256 private _reserve23;
103+
//uint256 internal _reserve22;
104+
uint256 internal _reserve23;
96105
uint256 private _reserve24;
97106
uint256 private _reserve25;
98107
uint256 private _reserve26;
@@ -223,6 +232,62 @@ contract SuperToken is
223232
return _STANDARD_DECIMALS;
224233
}
225234

235+
/**************************************************************************
236+
* ERC20 Permit (EIP-2612)
237+
*************************************************************************/
238+
239+
/// @dev EIP-2612 Permit
240+
function permit(
241+
address owner,
242+
address spender,
243+
uint256 value,
244+
uint256 deadline,
245+
uint8 v,
246+
bytes32 r,
247+
bytes32 s
248+
) public override {
249+
if (block.timestamp > deadline) revert SUPER_TOKEN_PERMIT_EXPIRED_SIGNATURE(deadline);
250+
251+
bytes32 structHash = keccak256(
252+
abi.encode(
253+
_PERMIT_TYPEHASH,
254+
owner,
255+
spender,
256+
value,
257+
_nonces[owner]++,
258+
deadline
259+
)
260+
);
261+
262+
bytes32 hash = MessageHashUtils.toTypedDataHash(DOMAIN_SEPARATOR(), structHash);
263+
264+
address signer = ECDSA.recover(hash, v, r, s);
265+
if (signer != owner) revert SUPER_TOKEN_PERMIT_INVALID_SIGNER(signer, owner);
266+
267+
_approve(owner, spender, value);
268+
}
269+
270+
/// @dev EIP-712 Domain Separator
271+
// solhint-disable func-name-mixedcase
272+
function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
273+
// TODO: can be optimized: provide immutable parts from constants
274+
return keccak256(
275+
abi.encode(
276+
// TYPE_HASH
277+
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
278+
keccak256("SuperToken"), // name
279+
keccak256("1"), // version
280+
block.chainid,
281+
address(this)
282+
)
283+
);
284+
}
285+
286+
/// @dev EIP-2612 Nonces
287+
function nonces(address owner) public view virtual returns (uint256) {
288+
return _nonces[owner];
289+
}
290+
226291
/**************************************************************************
227292
* (private) Token Logics
228293
*************************************************************************/
@@ -905,5 +970,4 @@ contract SuperToken is
905970
if (msg.sender != admin) revert SUPER_TOKEN_ONLY_ADMIN();
906971
_;
907972
}
908-
909973
}

packages/ethereum-contracts/foundry.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ optimizer_runs = 200
1414
remappings = [
1515
'@superfluid-finance/ethereum-contracts/contracts/=packages/ethereum-contracts/contracts/',
1616
'@superfluid-finance/solidity-semantic-money/src/=packages/solidity-semantic-money/src/',
17-
'@openzeppelin/=node_modules/@openzeppelin/',
17+
'@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/',
18+
'@openzeppelin/contracts-v5/=node_modules/@openzeppelin/contracts-v5/',
1819
'ds-test/=lib/forge-std/lib/ds-test/src/',
1920
'forge-std/=lib/forge-std/src/']
2021
out = 'packages/ethereum-contracts/build/foundry/default'

packages/ethereum-contracts/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"@decentral.ee/web3-helpers": "0.5.3",
77
"@nomiclabs/hardhat-ethers": "2.2.3",
88
"@openzeppelin/contracts": "4.9.6",
9+
"@openzeppelin/contracts-v5": "npm:@openzeppelin/contracts@5.2.0",
910
"@truffle/contract": "4.6.31",
1011
"ethereumjs-tx": "2.1.2",
1112
"ethereumjs-util": "7.1.5",

packages/ethereum-contracts/test/foundry/superfluid/SuperToken.t.sol

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
pragma solidity ^0.8.23;
33

44
import { Test } from "forge-std/Test.sol";
5+
import { console } from "forge-std/console.sol";
56
import { UUPSProxy } from "../../../contracts/upgradability/UUPSProxy.sol";
67
import { UUPSProxiable } from "../../../contracts/upgradability/UUPSProxiable.sol";
78
import { IERC20, ISuperToken, SuperToken, IConstantOutflowNFT, IConstantInflowNFT }
@@ -178,4 +179,77 @@ contract SuperTokenIntegrationTest is FoundrySuperfluidTester {
178179
"testOnlyHostCanUpdateCodeWhenNoAdmin: super token logic not updated correctly"
179180
);
180181
}
182+
183+
function testPermit(
184+
address relayer,
185+
uint256 signerPrivKey,
186+
uint256 amount,
187+
address spender,
188+
uint32 deadlineDelta
189+
) public {
190+
uint256 deadline = bound(deadlineDelta, block.timestamp, block.timestamp + deadlineDelta);
191+
amount = bound(amount, 1, type(uint96).max);
192+
signerPrivKey = bound(signerPrivKey, 1, type(uint128).max);
193+
address permitSigner = vm.addr(signerPrivKey);
194+
195+
(ISuperToken localSuperToken) = sfDeployer.deployPureSuperToken("Super MR", "MRx", amount * 2);
196+
localSuperToken.transfer(permitSigner, amount * 2);
197+
uint256 nonce = localSuperToken.nonces(permitSigner);
198+
// check nonce is 0
199+
assertEq(nonce, 0, "Nonce should be 0");
200+
201+
assertEq(localSuperToken.allowance(permitSigner, spender), 0, "Allowance should be 0");
202+
203+
bytes32 digest;
204+
// stack too deep avoidance gymnastics
205+
{
206+
// create permit digest
207+
bytes32 PERMIT_TYPEHASH =
208+
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
209+
bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, permitSigner, spender, amount, nonce, deadline));
210+
digest = keccak256(
211+
abi.encodePacked(
212+
"\x19\x01",
213+
localSuperToken.DOMAIN_SEPARATOR(),
214+
structHash
215+
)
216+
);
217+
}
218+
219+
// create signature
220+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivKey, digest);
221+
222+
vm.startPrank(relayer);
223+
224+
// expect revert if spender doesn't match
225+
if (spender != relayer) {
226+
vm.expectRevert();
227+
localSuperToken.permit(permitSigner, relayer, amount, deadline, v, r, s);
228+
}
229+
230+
// expect revert if amount doesn't match
231+
vm.expectRevert();
232+
localSuperToken.permit(permitSigner, spender, amount + 1, deadline, v, r, s);
233+
234+
// expect revert if signature is invalid
235+
vm.expectRevert();
236+
localSuperToken.permit(permitSigner, spender, amount, deadline, v + 1, r, s);
237+
238+
// expect revert if deadline is in the past
239+
uint256 prevBlockTS = block.timestamp;
240+
vm.warp(block.timestamp + deadline + 1);
241+
vm.expectRevert();
242+
localSuperToken.permit(permitSigner, spender, amount, deadline, v, r, s);
243+
// restore block timestamp
244+
vm.warp(prevBlockTS);
245+
246+
// succeed with correct parameters
247+
localSuperToken.permit(permitSigner, spender, amount, deadline, v, r, s);
248+
249+
vm.stopPrank();
250+
251+
// Verify expected state changes
252+
assertEq(localSuperToken.nonces(permitSigner), 1, "Nonce should be incremented");
253+
assertEq(localSuperToken.allowance(permitSigner, spender), amount, "Allowance should be set");
254+
}
181255
}

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3318,6 +3318,11 @@
33183318
find-up "^4.1.0"
33193319
fs-extra "^8.1.0"
33203320

3321+
"@openzeppelin/contracts-v5@npm:@openzeppelin/contracts@5.2.0":
3322+
version "5.2.0"
3323+
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.2.0.tgz#bd020694218202b811b0ea3eec07277814c658da"
3324+
integrity sha512-bxjNie5z89W1Ea0NZLZluFh8PrFNn9DH8DQlujEok2yjsOlraUPKID5p1Wk3qdNbf6XkQ1Os2RvfiHrrXLHWKA==
3325+
33213326
"@openzeppelin/contracts@4.9.6", "@openzeppelin/contracts@^4.9.6":
33223327
version "4.9.6"
33233328
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.6.tgz#2a880a24eb19b4f8b25adc2a5095f2aa27f39677"

0 commit comments

Comments
 (0)