Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

possible implementation of a FOTToken #27

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
97 changes: 97 additions & 0 deletions contracts/experimental/FOTToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity 0.8.19;

// This file contains everything we need for a customized Pure SuperToken.
// It imports needed dependencies from the Superfluid framework and OpenZeppelin.
// The custom logic is added to the proxy contract, which inherits from UUPSProxy.
// We also provide an interface for the custom token, which consists of the intersection of ISuperToken and the token's custom interface.

// This abstract contract provides storage padding for the proxy
import { CustomSuperTokenBase } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/CustomSuperTokenBase.sol";
// Implementation of UUPSPoxy (see https://eips.ethereum.org/EIPS/eip-1822)
import { UUPSProxy } from "@superfluid-finance/ethereum-contracts/contracts/upgradability/UUPSProxy.sol";
// Superfluid framework interfaces we need (incl. IERC20)
import { ISuperToken, ISuperTokenFactory, IERC20} from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol";
// Ownable for the admin interface - not needed for tokens without admin interface
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

// Custom interface of our token.
// Also includes methods of ISuperToken we intercept in order to change their behavior.
interface IFOTTokenCustom {
// admin interface
function setFeeConfig(uint256 _feePerTx, address _feeRecipient) external /*onlyOwner*/;

// subset of ISuperToken/IERC20 intercepted in the proxy in order to add a fee
function transferFrom(address holder, address recipient, uint256 amount) external returns (bool);
function transfer(address recipient, uint256 amount) external returns (bool);
}

/// @title FOT (Fee on Transfer) Token
/// Simple implementation of a Pure SuperToken taking a constant fee for every transfer operation.
/// @notice CustomSuperTokenBase MUST be the first inherited contract, otherwise the storage layout breaks.
contract FOTTokenProxy is CustomSuperTokenBase, UUPSProxy, Ownable, IFOTTokenCustom {
// Thanks to the storage padding provided by CustomSuperTokenBase, we can safely add storage variables here
uint256 public feePerTx; // amount detracted as fee per tx
address public feeRecipient; // receiver of the fee

// event emitted when the fee config is set
event FeeConfigSet(uint256 feePerTx, address feeRecipient);

constructor(uint256 _feePerTx, address _feeRecipient) {
setFeeConfig(_feePerTx, _feeRecipient);
}

// This shall be invoked exactly once after deployment, needed for the token contract to become operational.
function initialize(ISuperTokenFactory factory, string memory name, string memory symbol) external {
// This call to the factory invokes `UUPSProxy.initialize`, which connects the proxy to the canonical SuperToken implementation.
// It also emits an event which facilitates discovery of this token.
ISuperTokenFactory(factory).initializeCustomSuperToken(address(this));
// This initializes the token storage and sets the `initialized` flag of OpenZeppelin Initializable.
ISuperToken(address(this)).initialize(IERC20(address(0)), 18, name, symbol);
}

// ======= IFOTTokenCustom =======

// admin function to set the fee config, permissioned by Ownable
function setFeeConfig(uint256 _feePerTx, address _feeRecipient) public override onlyOwner {
feePerTx = _feePerTx;
feeRecipient = _feeRecipient;
emit FeeConfigSet(_feePerTx, _feeRecipient);
}

// intercepted `ERC20.transferFrom`
function transferFrom(address holder, address recipient, uint256 amount) external override returns (bool) {
_transferFrom(holder, recipient, amount);
return true;
}

// intercepted `ERC20.transfer`
function transfer(address recipient, uint256 amount) external override returns (bool) {
_transferFrom(msg.sender, recipient, amount);
return true;
}

// ======= Internal =======

// In order to achieve the desired behaviour of the intercepted transfer methods,
// we use the "self" methods of the canonical SuperToken implementation to do 2 transfers: one to the actual recipient and one to the fee recipient.
// The self methods can only be called by the token contract itself (modifier `onlySelf`).
// This works because by triggering an external call via `this.method`, we invoke the fallback function of this proxy,
// which does a delegate call to the canonical implementation it points to.
// In the context of a delegate call, `address(this)` resolves to the address of the caller, which in this case is this proxy contract.
function _transferFrom(address holder, address recipient, uint256 amount) internal {
// get the fee
ISuperToken(address(this)).selfTransferFrom(holder, msg.sender, feeRecipient, feePerTx);

// do the actual tx
ISuperToken(address(this)).selfTransferFrom(holder, msg.sender, recipient, amount);
}
}

// This interface makes it more convenient for Dapps to interface with the token,
// unifying the APIs implemented by the proxy and by the canonical implementation into a single interface.
interface IFOTToken is ISuperToken, IFOTTokenCustom {
// We need to re-declare the methods present in both base interfaces to avoid compiler complaints.
function transferFrom(address holder, address recipient, uint256 amount) external override(ISuperToken, IFOTTokenCustom) returns (bool);
function transfer(address recipient, uint256 amount) external override(ISuperToken, IFOTTokenCustom) returns (bool);
}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
},
"dependencies": {
"@openzeppelin/contracts": "^4.7.3",
"@superfluid-finance/ethereum-contracts": "^1.4.3",
"truffle-plugin-verify": "^0.6.0"
"@superfluid-finance/ethereum-contracts": "^1.8.1",
"@superfluid-finance/metadata": "^1.1.13",
"truffle-plugin-verify": "0.6.5"
},
"devDependencies": {
"@decentral.ee/web3-helpers": "^0.5.3",
Expand Down
98 changes: 98 additions & 0 deletions test/foundry/FOTToken.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity 0.8.19;

import "forge-std/Test.sol";
import { FOTTokenProxy, IFOTToken } from "../../contracts/experimental/FOTToken.sol";
import { ISuperfluid } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol";
import { SuperfluidFrameworkDeployer } from "@superfluid-finance/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeployer.sol";
import { ERC1820RegistryCompiled } from "@superfluid-finance/ethereum-contracts/contracts/libs/ERC1820RegistryCompiled.sol";

contract FOTTokenTest is Test {
IFOTToken public fotToken;
SuperfluidFrameworkDeployer.Framework sf;
uint256 public txFee = 1e16;

address public admin = address(0x42);
address public txFeeRecipient = address(0x420);
address public alice = address(0x421);
address public bob = address(0x422);
address public dan = address(0x423);

constructor() {
vm.etch(ERC1820RegistryCompiled.at, ERC1820RegistryCompiled.bin);
SuperfluidFrameworkDeployer sfDeployer = new SuperfluidFrameworkDeployer();
sfDeployer.deployTestFramework();
sf = sfDeployer.getFramework();
}

function setUp() public {
vm.startPrank(admin);
FOTTokenProxy fotTokenProxy = new FOTTokenProxy(txFee, txFeeRecipient);
fotTokenProxy.initialize(sf.superTokenFactory, "FOTToken", "FOT");
fotToken = IFOTToken(address(fotTokenProxy));
}

function testTransfer() public {
deal(address(fotToken), alice, 100 ether);

vm.startPrank(alice);
fotToken.transfer(bob, 1 ether);

assertEq(fotToken.balanceOf(alice), 99 ether - txFee);
assertEq(fotToken.balanceOf(bob), 1 ether);
assertEq(fotToken.balanceOf(txFeeRecipient), txFee);
}

function testTranferFromPermissioning() public {
deal(address(fotToken), alice, 100 ether);

vm.startPrank(dan);
vm.expectRevert("SuperToken: transfer amount exceeds allowance");
fotToken.transferFrom(alice, bob, 1 ether);
vm.stopPrank();

// give allowance
vm.prank(alice);
fotToken.approve(dan, 1 ether);

// should still fail because the fee isn't covered by the allowance
vm.startPrank(dan);
vm.expectRevert("SuperToken: transfer amount exceeds allowance");
fotToken.transferFrom(alice, bob, 1 ether);
vm.stopPrank();

// add the fee amount
vm.prank(alice);
fotToken.increaseAllowance(dan, txFee);

// now we make everybody happy
vm.startPrank(dan);
fotToken.transferFrom(alice, bob, 1 ether);

assertEq(fotToken.balanceOf(alice), 99 ether - txFee);
assertEq(fotToken.balanceOf(bob), 1 ether);
assertEq(fotToken.balanceOf(txFeeRecipient), txFee);
}

function testChangeFeeConfig() external {
// getting greedy
uint256 newTxFee = 2e16;

vm.startPrank(dan);
vm.expectRevert("Ownable: caller is not the owner"); // dan isn't the owner
// casting to payable needed because the proxy contains a payable fallback function
fotToken.setFeeConfig(newTxFee, dan);
vm.stopPrank();

vm.startPrank(admin);
fotToken.setFeeConfig(newTxFee, dan);

deal(address(fotToken), alice, 100 ether);
vm.stopPrank();

vm.startPrank(alice);
fotToken.transfer(bob, 1 ether);

assertEq(fotToken.balanceOf(dan), newTxFee);
}
}
Loading
Loading