Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
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
5 changes: 5 additions & 0 deletions packages/ethereum-contracts/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ All notable changes to the ethereum-contracts will be documented in this file.

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

## [unreleased]

### Added
- `SuperToken` now implements [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) (permit extension for EIP-20 signed approvals)

## [v1.12.1]

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ 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/token/ERC20/extensions/IERC20Permit.sol";
import { IERC5267 } from "@openzeppelin/contracts/interfaces/IERC5267.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 +13,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, IERC5267 {

/**************************************************************************
* 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
117 changes: 111 additions & 6 deletions packages/ethereum-contracts/contracts/superfluid/SuperToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import { IERC777Recipient } from "@openzeppelin/contracts/token/ERC777/IERC777Recipient.sol";
import { IERC777Sender } from "@openzeppelin/contracts/token/ERC777/IERC777Sender.sol";
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

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

using SafeMath for uint256;
using SafeCast for uint256;
using Address for address;
Expand All @@ -49,6 +49,14 @@ 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)");
bytes32 constant private _EIP712_DOMAIN_TYPEHASH =
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");

string constant private _EIP712_VERSION = "1";

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

Expand Down Expand Up @@ -84,15 +92,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 @@ -181,14 +192,14 @@ contract SuperToken is
UUPSProxiable._updateCodeAddress(newAddress);
}

function changeAdmin(address newAdmin) external override onlyAdmin {
function changeAdmin(address newAdmin) external virtual override onlyAdmin {
address oldAdmin = _getAdmin();
_setAdmin(newAdmin);

emit AdminChanged(oldAdmin, newAdmin);
}

function getAdmin() external view override returns (address) {
function getAdmin() external view virtual override returns (address) {
return _getAdmin();
}

Expand Down Expand Up @@ -223,6 +234,101 @@ 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 virtual 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 domainSeparator = DOMAIN_SEPARATOR();
// Get the keccak256 digest of the EIP-712 typed data (ERC-191 version `0x01`).
// solhint-disable-next-line max-line-length
// Snippet taken from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.2.0/contracts/utils/cryptography/MessageHashUtils.sol
bytes32 hash;
assembly ("memory-safe") {
let ptr := mload(0x40)
mstore(ptr, hex"19_01")
mstore(add(ptr, 0x02), domainSeparator)
mstore(add(ptr, 0x22), structHash)
hash := keccak256(ptr, 0x42)
}

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 override returns (bytes32) {
// TODO: can be optimized: provide immutable parts from constants
return keccak256(
abi.encode(
_EIP712_DOMAIN_TYPEHASH,
keccak256(bytes(_name)),
keccak256(bytes(_EIP712_VERSION)),
block.chainid,
address(this)
)
);
}

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

/// @dev EIP-5267: Retrieval of EIP-712 domain
function eip712Domain()
public
view
virtual
override
returns
(
bytes1 fields,
/* commented out to avoid warning of name clash with name() */
string memory /*name*/,
string memory version,
uint256 chainId,
address verifyingContract,
bytes32 salt,
uint256[] memory extensions
)
{
return (
hex"0f", // 01111 - field "salt" not present
_name,
_EIP712_VERSION,
block.chainid,
address(this), // verifyingContract
bytes32(0), // salt
new uint256[](0) // extensions
);
}

/**************************************************************************
* (private) Token Logics
*************************************************************************/
Expand Down Expand Up @@ -905,5 +1011,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
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ contract SuperTokenIntegrationTest is FoundrySuperfluidTester {
}

function testToUnderlyingAmountWithUpgrade(uint8 decimals, uint256 amount) public {
vm.assume(amount < type(uint64).max);
amount = bound(amount, 0, type(uint64).max);
// We assume that most underlying tokens will not have more than 32 decimals
vm.assume(decimals <= 32);
(TestToken localToken, ISuperToken localSuperToken) =
Expand All @@ -42,10 +42,10 @@ contract SuperTokenIntegrationTest is FoundrySuperfluidTester {
function testToUnderlyingAmountWithDowngrade(uint8 decimals, uint256 upgradeAmount, uint256 downgradeAmount)
public
{
vm.assume(upgradeAmount < type(uint64).max);
upgradeAmount = bound(upgradeAmount, 0, type(uint64).max);
// We assume that most underlying tokens will not have more than 32 decimals
vm.assume(decimals <= 32);
vm.assume(downgradeAmount < upgradeAmount);
downgradeAmount = bound(downgradeAmount, 0, upgradeAmount);
(TestToken localToken, ISuperToken localSuperToken) =
sfDeployer.deployWrapperSuperToken("FTT", "FTT", decimals, type(uint256).max, address(0));
(uint256 underlyingAmount, uint256 adjustedAmount) = localSuperToken.toUnderlyingAmount(upgradeAmount);
Expand Down Expand Up @@ -178,4 +178,81 @@ 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);
// zero address is not a valid signer
vm.assume(permitSigner != address(0));
// SuperToken doesn't allow approval to zero address
vm.assume(spender != address(0));

(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");
}
}