Skip to content

Commit 419670d

Browse files
authored
Merge branch 'dev' into vesting-scheduler-v3
2 parents 13991df + 4e8142f commit 419670d

File tree

7 files changed

+218
-27
lines changed

7 files changed

+218
-27
lines changed

packages/ethereum-contracts/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ All notable changes to the ethereum-contracts will be documented in this file.
33

44
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
55

6+
## [unreleased]
7+
8+
### Added
9+
- `SuperToken` now implements [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) (permit extension for EIP-20 signed approvals)
10+
611
## [v1.12.1]
712

813
### Added

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

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ 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/token/ERC20/extensions/IERC20Permit.sol";
7+
import { IERC5267 } from "@openzeppelin/contracts/interfaces/IERC5267.sol";
68
import { IERC777 } from "@openzeppelin/contracts/token/ERC777/IERC777.sol";
79
import { IPoolAdminNFT } from "../agreements/gdav1/IPoolAdminNFT.sol";
810
import { IPoolMemberNFT } from "../agreements/gdav1/IPoolMemberNFT.sol";
@@ -11,25 +13,27 @@ import { IPoolMemberNFT } from "../agreements/gdav1/IPoolMemberNFT.sol";
1113
* @title Super token (Superfluid Token + ERC20 + ERC777) interface
1214
* @author Superfluid
1315
*/
14-
interface ISuperToken is ISuperfluidToken, IERC20Metadata, IERC777 {
16+
interface ISuperToken is ISuperfluidToken, IERC20Metadata, IERC777, IERC20Permit, IERC5267 {
1517

1618
/**************************************************************************
1719
* Errors
1820
*************************************************************************/
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
21+
error SUPER_TOKEN_CALLER_IS_NOT_OPERATOR_FOR_HOLDER(); // 0xf7f02227
22+
error SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT(); // 0xfe737d05
23+
error SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED(); // 0xe3e13698
24+
error SUPER_TOKEN_NO_UNDERLYING_TOKEN(); // 0xf79cf656
25+
error SUPER_TOKEN_ONLY_SELF(); // 0x7ffa6648
26+
error SUPER_TOKEN_ONLY_ADMIN(); // 0x0484acab
27+
error SUPER_TOKEN_ONLY_GOV_OWNER(); // 0xd9c7ed08
28+
error SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS(); // 0x81638627
29+
error SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS(); // 0xdf070274
30+
error SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS(); // 0xba2ab184
31+
error SUPER_TOKEN_MINT_TO_ZERO_ADDRESS(); // 0x0d243157
32+
error SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS(); // 0xeecd6c9b
33+
error SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS(); // 0xe219bd39
34+
error SUPER_TOKEN_NFT_PROXY_ADDRESS_CHANGED(); // 0xef1b6ddf
35+
error SUPER_TOKEN_PERMIT_EXPIRED_SIGNATURE(uint256 deadline); // 0x6e72b90f
36+
error SUPER_TOKEN_PERMIT_INVALID_SIGNER(address signer, address owner); // 0xb6422105
3337

3438
/**
3539
* @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: 110 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
2020
import { IERC777Recipient } from "@openzeppelin/contracts/token/ERC777/IERC777Recipient.sol";
2121
import { IERC777Sender } from "@openzeppelin/contracts/token/ERC777/IERC777Sender.sol";
2222
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
23+
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
2324

2425
// placeholder types needed as an intermediate step before complete removal of FlowNFTs
2526
// solhint-disable-next-line no-empty-blocks
@@ -37,7 +38,6 @@ contract SuperToken is
3738
SuperfluidToken,
3839
ISuperToken
3940
{
40-
4141
using SafeMath for uint256;
4242
using SafeCast for uint256;
4343
using Address for address;
@@ -49,6 +49,14 @@ contract SuperToken is
4949

5050
uint8 constant private _STANDARD_DECIMALS = 18;
5151

52+
// EIP-712 permit typehash
53+
bytes32 constant private _PERMIT_TYPEHASH =
54+
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
55+
bytes32 constant private _EIP712_DOMAIN_TYPEHASH =
56+
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
57+
58+
string constant private _EIP712_VERSION = "1";
59+
5260
// solhint-disable-next-line var-name-mixedcase
5361
IConstantOutflowNFT immutable public CONSTANT_OUTFLOW_NFT;
5462

@@ -84,15 +92,17 @@ contract SuperToken is
8492
/// @dev ERC777 operators support data
8593
ERC777Helper.Operators internal _operators;
8694

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

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

94-
uint256 internal _reserve22;
95-
uint256 private _reserve23;
105+
uint256 internal _reserve23;
96106
uint256 private _reserve24;
97107
uint256 private _reserve25;
98108
uint256 private _reserve26;
@@ -181,14 +191,14 @@ contract SuperToken is
181191
UUPSProxiable._updateCodeAddress(newAddress);
182192
}
183193

184-
function changeAdmin(address newAdmin) external override onlyAdmin {
194+
function changeAdmin(address newAdmin) external virtual override onlyAdmin {
185195
address oldAdmin = _getAdmin();
186196
_setAdmin(newAdmin);
187197

188198
emit AdminChanged(oldAdmin, newAdmin);
189199
}
190200

191-
function getAdmin() external view override returns (address) {
201+
function getAdmin() external view virtual override returns (address) {
192202
return _getAdmin();
193203
}
194204

@@ -223,6 +233,101 @@ contract SuperToken is
223233
return _STANDARD_DECIMALS;
224234
}
225235

236+
/**************************************************************************
237+
* ERC20 Permit (EIP-2612)
238+
*************************************************************************/
239+
240+
/// @dev EIP-2612 Permit
241+
function permit(
242+
address owner,
243+
address spender,
244+
uint256 value,
245+
uint256 deadline,
246+
uint8 v,
247+
bytes32 r,
248+
bytes32 s
249+
) public virtual override {
250+
if (block.timestamp > deadline) revert SUPER_TOKEN_PERMIT_EXPIRED_SIGNATURE(deadline);
251+
252+
bytes32 structHash = keccak256(
253+
abi.encode(
254+
_PERMIT_TYPEHASH,
255+
owner,
256+
spender,
257+
value,
258+
_nonces[owner]++,
259+
deadline
260+
)
261+
);
262+
263+
bytes32 domainSeparator = DOMAIN_SEPARATOR();
264+
// Get the keccak256 digest of the EIP-712 typed data (ERC-191 version `0x01`).
265+
// solhint-disable-next-line max-line-length
266+
// Snippet taken from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.2.0/contracts/utils/cryptography/MessageHashUtils.sol
267+
bytes32 hash;
268+
assembly ("memory-safe") {
269+
let ptr := mload(0x40)
270+
mstore(ptr, hex"19_01")
271+
mstore(add(ptr, 0x02), domainSeparator)
272+
mstore(add(ptr, 0x22), structHash)
273+
hash := keccak256(ptr, 0x42)
274+
}
275+
276+
address signer = ECDSA.recover(hash, v, r, s);
277+
if (signer != owner) revert SUPER_TOKEN_PERMIT_INVALID_SIGNER(signer, owner);
278+
279+
_approve(owner, spender, value);
280+
}
281+
282+
/// @dev EIP-712 Domain Separator
283+
// solhint-disable func-name-mixedcase
284+
function DOMAIN_SEPARATOR() public view virtual override returns (bytes32) {
285+
// Here we could squeeze out some gas by using pre-computed hashes
286+
return keccak256(
287+
abi.encode(
288+
_EIP712_DOMAIN_TYPEHASH,
289+
keccak256(bytes(_name)),
290+
keccak256(bytes(_EIP712_VERSION)),
291+
block.chainid,
292+
address(this)
293+
)
294+
);
295+
}
296+
297+
/// @dev EIP-2612 Nonces
298+
function nonces(address owner) public view virtual override returns (uint256) {
299+
return _nonces[owner];
300+
}
301+
302+
/// @dev EIP-5267: Retrieval of EIP-712 domain
303+
function eip712Domain()
304+
public
305+
view
306+
virtual
307+
override
308+
returns
309+
(
310+
bytes1 fields,
311+
/* commented out to avoid warning of name clash with name() */
312+
string memory /*name*/,
313+
string memory version,
314+
uint256 chainId,
315+
address verifyingContract,
316+
bytes32 salt,
317+
uint256[] memory extensions
318+
)
319+
{
320+
return (
321+
hex"0f", // 01111 - field "salt" not present
322+
_name,
323+
_EIP712_VERSION,
324+
block.chainid,
325+
address(this), // verifyingContract
326+
bytes32(0), // salt
327+
new uint256[](0) // extensions
328+
);
329+
}
330+
226331
/**************************************************************************
227332
* (private) Token Logics
228333
*************************************************************************/
@@ -905,5 +1010,4 @@ contract SuperToken is
9051010
if (msg.sender != admin) revert SUPER_TOKEN_ONLY_ADMIN();
9061011
_;
9071012
}
908-
9091013
}

packages/ethereum-contracts/foundry.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ 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/',
1818
'ds-test/=lib/forge-std/lib/ds-test/src/',
1919
'forge-std/=lib/forge-std/src/']
2020
out = 'packages/ethereum-contracts/build/foundry/default'

packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste
446446
vm.assume(distributionFlowRate < minDepositFlowRate);
447447
vm.assume(distributionFlowRate > 0);
448448
vm.assume(member != address(pool));
449+
vm.assume(member != address(freePool)); // yes, this also happened
449450
vm.assume(member != address(0));
450451

451452
_addAccount(member);

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

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ contract SuperTokenIntegrationTest is FoundrySuperfluidTester {
2424
}
2525

2626
function testToUnderlyingAmountWithUpgrade(uint8 decimals, uint256 amount) public {
27-
vm.assume(amount < type(uint64).max);
27+
amount = bound(amount, 0, type(uint64).max);
2828
// We assume that most underlying tokens will not have more than 32 decimals
2929
vm.assume(decimals <= 32);
3030
(TestToken localToken, ISuperToken localSuperToken) =
@@ -42,10 +42,10 @@ contract SuperTokenIntegrationTest is FoundrySuperfluidTester {
4242
function testToUnderlyingAmountWithDowngrade(uint8 decimals, uint256 upgradeAmount, uint256 downgradeAmount)
4343
public
4444
{
45-
vm.assume(upgradeAmount < type(uint64).max);
45+
upgradeAmount = bound(upgradeAmount, 0, type(uint64).max);
4646
// We assume that most underlying tokens will not have more than 32 decimals
4747
vm.assume(decimals <= 32);
48-
vm.assume(downgradeAmount < upgradeAmount);
48+
downgradeAmount = bound(downgradeAmount, 0, upgradeAmount);
4949
(TestToken localToken, ISuperToken localSuperToken) =
5050
sfDeployer.deployWrapperSuperToken("FTT", "FTT", decimals, type(uint256).max, address(0));
5151
(uint256 underlyingAmount, uint256 adjustedAmount) = localSuperToken.toUnderlyingAmount(upgradeAmount);
@@ -178,4 +178,81 @@ contract SuperTokenIntegrationTest is FoundrySuperfluidTester {
178178
"testOnlyHostCanUpdateCodeWhenNoAdmin: super token logic not updated correctly"
179179
);
180180
}
181+
182+
function testPermit(
183+
address relayer,
184+
uint256 signerPrivKey,
185+
uint256 amount,
186+
address spender,
187+
uint32 deadlineDelta
188+
) public {
189+
uint256 deadline = bound(deadlineDelta, block.timestamp, block.timestamp + deadlineDelta);
190+
amount = bound(amount, 1, type(uint96).max);
191+
signerPrivKey = bound(signerPrivKey, 1, type(uint128).max);
192+
address permitSigner = vm.addr(signerPrivKey);
193+
// zero address is not a valid signer
194+
vm.assume(permitSigner != address(0));
195+
// SuperToken doesn't allow approval to zero address
196+
vm.assume(spender != address(0));
197+
198+
(ISuperToken localSuperToken) = sfDeployer.deployPureSuperToken("Super MR", "MRx", amount * 2);
199+
localSuperToken.transfer(permitSigner, amount * 2);
200+
uint256 nonce = localSuperToken.nonces(permitSigner);
201+
// check nonce is 0
202+
assertEq(nonce, 0, "Nonce should be 0");
203+
204+
assertEq(localSuperToken.allowance(permitSigner, spender), 0, "Allowance should be 0");
205+
206+
bytes32 digest;
207+
// stack too deep avoidance gymnastics
208+
{
209+
// create permit digest
210+
bytes32 PERMIT_TYPEHASH =
211+
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
212+
bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, permitSigner, spender, amount, nonce, deadline));
213+
digest = keccak256(
214+
abi.encodePacked(
215+
"\x19\x01",
216+
localSuperToken.DOMAIN_SEPARATOR(),
217+
structHash
218+
)
219+
);
220+
}
221+
222+
// create signature
223+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivKey, digest);
224+
225+
vm.startPrank(relayer);
226+
227+
// expect revert if spender doesn't match
228+
if (spender != relayer) {
229+
vm.expectRevert();
230+
localSuperToken.permit(permitSigner, relayer, amount, deadline, v, r, s);
231+
}
232+
233+
// expect revert if amount doesn't match
234+
vm.expectRevert();
235+
localSuperToken.permit(permitSigner, spender, amount + 1, deadline, v, r, s);
236+
237+
// expect revert if signature is invalid
238+
vm.expectRevert();
239+
localSuperToken.permit(permitSigner, spender, amount, deadline, v + 1, r, s);
240+
241+
// expect revert if deadline is in the past
242+
uint256 prevBlockTS = block.timestamp;
243+
vm.warp(block.timestamp + deadline + 1);
244+
vm.expectRevert();
245+
localSuperToken.permit(permitSigner, spender, amount, deadline, v, r, s);
246+
// restore block timestamp
247+
vm.warp(prevBlockTS);
248+
249+
// succeed with correct parameters
250+
localSuperToken.permit(permitSigner, spender, amount, deadline, v, r, s);
251+
252+
vm.stopPrank();
253+
254+
// Verify expected state changes
255+
assertEq(localSuperToken.nonces(permitSigner), 1, "Nonce should be incremented");
256+
assertEq(localSuperToken.allowance(permitSigner, spender), amount, "Allowance should be set");
257+
}
181258
}

0 commit comments

Comments
 (0)