Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity >= 0.8.11;

import { ISuperfluidToken } from "./ISuperfluidToken.sol";
import { IERC20, IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { IERC20Permit } from "@openzeppelin/contracts-v5/token/ERC20/extensions/IERC20Permit.sol";
import { IERC777 } from "@openzeppelin/contracts/token/ERC777/IERC777.sol";
import { IPoolAdminNFT } from "../agreements/gdav1/IPoolAdminNFT.sol";
import { IPoolMemberNFT } from "../agreements/gdav1/IPoolMemberNFT.sol";
Expand All @@ -11,25 +12,27 @@ import { IPoolMemberNFT } from "../agreements/gdav1/IPoolMemberNFT.sol";
* @title Super token (Superfluid Token + ERC20 + ERC777) interface
* @author Superfluid
*/
interface ISuperToken is ISuperfluidToken, IERC20Metadata, IERC777 {
interface ISuperToken is ISuperfluidToken, IERC20Metadata, IERC777, IERC20Permit {

/**************************************************************************
* Errors
*************************************************************************/
error SUPER_TOKEN_CALLER_IS_NOT_OPERATOR_FOR_HOLDER(); // 0xf7f02227
error SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT(); // 0xfe737d05
error SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED(); // 0xe3e13698
error SUPER_TOKEN_NO_UNDERLYING_TOKEN(); // 0xf79cf656
error SUPER_TOKEN_ONLY_SELF(); // 0x7ffa6648
error SUPER_TOKEN_ONLY_ADMIN(); // 0x0484acab
error SUPER_TOKEN_ONLY_GOV_OWNER(); // 0xd9c7ed08
error SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS(); // 0x81638627
error SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS(); // 0xdf070274
error SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS(); // 0xba2ab184
error SUPER_TOKEN_MINT_TO_ZERO_ADDRESS(); // 0x0d243157
error SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS(); // 0xeecd6c9b
error SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS(); // 0xe219bd39
error SUPER_TOKEN_NFT_PROXY_ADDRESS_CHANGED(); // 0x6bef249d
error SUPER_TOKEN_CALLER_IS_NOT_OPERATOR_FOR_HOLDER(); // 0xf7f02227
error SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT(); // 0xfe737d05
error SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED(); // 0xe3e13698
error SUPER_TOKEN_NO_UNDERLYING_TOKEN(); // 0xf79cf656
error SUPER_TOKEN_ONLY_SELF(); // 0x7ffa6648
error SUPER_TOKEN_ONLY_ADMIN(); // 0x0484acab
error SUPER_TOKEN_ONLY_GOV_OWNER(); // 0xd9c7ed08
error SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS(); // 0x81638627
error SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS(); // 0xdf070274
error SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS(); // 0xba2ab184
error SUPER_TOKEN_MINT_TO_ZERO_ADDRESS(); // 0x0d243157
error SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS(); // 0xeecd6c9b
error SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS(); // 0xe219bd39
error SUPER_TOKEN_NFT_PROXY_ADDRESS_CHANGED(); // 0xef1b6ddf
error SUPER_TOKEN_PERMIT_EXPIRED_SIGNATURE(uint256 deadline); // 0x6e72b90f
error SUPER_TOKEN_PERMIT_INVALID_SIGNER(address signer, address owner); // 0xb6422105

/**
* @dev Initialize the contract
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ contract SuperTokenStorageLayoutTester is SuperToken {
require (slot == 18 && offset == 0, "_operators changed location");
// uses 4 slots

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

assembly { slot:= _reserve31.slot offset := _reserve31.offset }
require (slot == 31 && offset == 0, "_reserve31 changed location");
Expand Down
72 changes: 68 additions & 4 deletions packages/ethereum-contracts/contracts/superfluid/SuperToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import { IERC777Recipient } from "@openzeppelin/contracts/token/ERC777/IERC777Re
import { IERC777Sender } from "@openzeppelin/contracts/token/ERC777/IERC777Sender.sol";
import { Address } from "@openzeppelin/contracts/utils/Address.sol";

import { ECDSA } from "@openzeppelin/contracts-v5/utils/cryptography/ECDSA.sol";
import { MessageHashUtils } from "@openzeppelin/contracts-v5/utils/cryptography/MessageHashUtils.sol";

// placeholder types needed as an intermediate step before complete removal of FlowNFTs
// solhint-disable-next-line no-empty-blocks
interface IConstantOutflowNFT {}
Expand All @@ -37,7 +40,6 @@ contract SuperToken is
SuperfluidToken,
ISuperToken
{

using SafeMath for uint256;
using SafeCast for uint256;
using Address for address;
Expand All @@ -49,6 +51,10 @@ contract SuperToken is

uint8 constant private _STANDARD_DECIMALS = 18;

// EIP-712 permit typehash
bytes32 constant private _PERMIT_TYPEHASH =
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");

// solhint-disable-next-line var-name-mixedcase
IConstantOutflowNFT immutable public CONSTANT_OUTFLOW_NFT;

Expand Down Expand Up @@ -84,15 +90,18 @@ contract SuperToken is
/// @dev ERC777 operators support data
ERC777Helper.Operators internal _operators;

/// @dev ERC20 Nonces for EIP-2612 (permit)
mapping(address account => uint256) internal _nonces;

// NOTE: for future compatibility, these are reserved solidity slots
// The sub-class of SuperToken solidity slot will start after _reserve22

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

uint256 internal _reserve22;
uint256 private _reserve23;
//uint256 internal _reserve22;
uint256 internal _reserve23;
uint256 private _reserve24;
uint256 private _reserve25;
uint256 private _reserve26;
Expand Down Expand Up @@ -223,6 +232,62 @@ contract SuperToken is
return _STANDARD_DECIMALS;
}

/**************************************************************************
* ERC20 Permit (EIP-2612)
*************************************************************************/

/// @dev EIP-2612 Permit
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public override {
if (block.timestamp > deadline) revert SUPER_TOKEN_PERMIT_EXPIRED_SIGNATURE(deadline);

bytes32 structHash = keccak256(
abi.encode(
_PERMIT_TYPEHASH,
owner,
spender,
value,
_nonces[owner]++,
deadline
)
);

bytes32 hash = MessageHashUtils.toTypedDataHash(DOMAIN_SEPARATOR(), structHash);

address signer = ECDSA.recover(hash, v, r, s);
if (signer != owner) revert SUPER_TOKEN_PERMIT_INVALID_SIGNER(signer, owner);

_approve(owner, spender, value);
}

/// @dev EIP-712 Domain Separator
// solhint-disable func-name-mixedcase
function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
// TODO: can be optimized: provide immutable parts from constants
return keccak256(
abi.encode(
// TYPE_HASH
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256("SuperToken"), // name
keccak256("1"), // version
block.chainid,
address(this)
)
);
}

/// @dev EIP-2612 Nonces
function nonces(address owner) public view virtual returns (uint256) {
return _nonces[owner];
}

/**************************************************************************
* (private) Token Logics
*************************************************************************/
Expand Down Expand Up @@ -905,5 +970,4 @@ contract SuperToken is
if (msg.sender != admin) revert SUPER_TOKEN_ONLY_ADMIN();
_;
}

}
3 changes: 2 additions & 1 deletion packages/ethereum-contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ optimizer_runs = 200
remappings = [
'@superfluid-finance/ethereum-contracts/contracts/=packages/ethereum-contracts/contracts/',
'@superfluid-finance/solidity-semantic-money/src/=packages/solidity-semantic-money/src/',
'@openzeppelin/=node_modules/@openzeppelin/',
'@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/',
'@openzeppelin/contracts-v5/=node_modules/@openzeppelin/contracts-v5/',
'ds-test/=lib/forge-std/lib/ds-test/src/',
'forge-std/=lib/forge-std/src/']
out = 'packages/ethereum-contracts/build/foundry/default'
Expand Down
1 change: 1 addition & 0 deletions packages/ethereum-contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"@decentral.ee/web3-helpers": "0.5.3",
"@nomiclabs/hardhat-ethers": "2.2.3",
"@openzeppelin/contracts": "4.9.6",
"@openzeppelin/contracts-v5": "npm:@openzeppelin/contracts@5.2.0",
"@truffle/contract": "4.6.31",
"ethereumjs-tx": "2.1.2",
"ethereumjs-util": "7.1.5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.23;

import { Test } from "forge-std/Test.sol";
import { console } from "forge-std/console.sol";
import { UUPSProxy } from "../../../contracts/upgradability/UUPSProxy.sol";
import { UUPSProxiable } from "../../../contracts/upgradability/UUPSProxiable.sol";
import { IERC20, ISuperToken, SuperToken, IConstantOutflowNFT, IConstantInflowNFT }
Expand Down Expand Up @@ -178,4 +179,77 @@ contract SuperTokenIntegrationTest is FoundrySuperfluidTester {
"testOnlyHostCanUpdateCodeWhenNoAdmin: super token logic not updated correctly"
);
}

function testPermit(
address relayer,
uint256 signerPrivKey,
uint256 amount,
address spender,
uint32 deadlineDelta
) public {
uint256 deadline = bound(deadlineDelta, block.timestamp, block.timestamp + deadlineDelta);
amount = bound(amount, 1, type(uint96).max);
signerPrivKey = bound(signerPrivKey, 1, type(uint128).max);
address permitSigner = vm.addr(signerPrivKey);

(ISuperToken localSuperToken) = sfDeployer.deployPureSuperToken("Super MR", "MRx", amount * 2);
localSuperToken.transfer(permitSigner, amount * 2);
uint256 nonce = localSuperToken.nonces(permitSigner);
// check nonce is 0
assertEq(nonce, 0, "Nonce should be 0");

assertEq(localSuperToken.allowance(permitSigner, spender), 0, "Allowance should be 0");

bytes32 digest;
// stack too deep avoidance gymnastics
{
// create permit digest
bytes32 PERMIT_TYPEHASH =
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, permitSigner, spender, amount, nonce, deadline));
digest = keccak256(
abi.encodePacked(
"\x19\x01",
localSuperToken.DOMAIN_SEPARATOR(),
structHash
)
);
}

// create signature
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivKey, digest);

vm.startPrank(relayer);

// expect revert if spender doesn't match
if (spender != relayer) {
vm.expectRevert();
localSuperToken.permit(permitSigner, relayer, amount, deadline, v, r, s);
}

// expect revert if amount doesn't match
vm.expectRevert();
localSuperToken.permit(permitSigner, spender, amount + 1, deadline, v, r, s);

// expect revert if signature is invalid
vm.expectRevert();
localSuperToken.permit(permitSigner, spender, amount, deadline, v + 1, r, s);

// expect revert if deadline is in the past
uint256 prevBlockTS = block.timestamp;
vm.warp(block.timestamp + deadline + 1);
vm.expectRevert();
localSuperToken.permit(permitSigner, spender, amount, deadline, v, r, s);
// restore block timestamp
vm.warp(prevBlockTS);

// succeed with correct parameters
localSuperToken.permit(permitSigner, spender, amount, deadline, v, r, s);

vm.stopPrank();

// Verify expected state changes
assertEq(localSuperToken.nonces(permitSigner), 1, "Nonce should be incremented");
assertEq(localSuperToken.allowance(permitSigner, spender), amount, "Allowance should be set");
}
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3318,6 +3318,11 @@
find-up "^4.1.0"
fs-extra "^8.1.0"

"@openzeppelin/contracts-v5@npm:@openzeppelin/contracts@5.2.0":
version "5.2.0"
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.2.0.tgz#bd020694218202b811b0ea3eec07277814c658da"
integrity sha512-bxjNie5z89W1Ea0NZLZluFh8PrFNn9DH8DQlujEok2yjsOlraUPKID5p1Wk3qdNbf6XkQ1Os2RvfiHrrXLHWKA==

"@openzeppelin/contracts@4.9.6", "@openzeppelin/contracts@^4.9.6":
version "4.9.6"
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.6.tgz#2a880a24eb19b4f8b25adc2a5095f2aa27f39677"
Expand Down
Loading