chore: introduce SNT V2 (#1)

This introduces an SNT V2 token implementation. It inherits from
`MiniMeToken` and also comes with an `SNTTestnetController` that can be
used on test networks for allowing minting of tokens by anyone.

This makes the token controller more general and introduces an `open`
property to enable/disable the faucet.

Also adds a `destroyTokens()` function for completeness' sake.
This commit is contained in:
r4bbit 2023-11-28 11:04:03 +01:00 committed by GitHub
parent 3f2175ac56
commit 23de68feb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 386 additions and 139 deletions

View File

@ -1 +1,10 @@
FooTest:test_Example() (gas: 8662) TestDestroyTokens:testDeployment() (gas: 47764)
TestDestroyTokens:test_DestroyTokenFaucetIsOpen() (gas: 133862)
TestDestroyTokens:test_DestroyTokensAsOwner() (gas: 131266)
TestDestroyTokens:test_RevertWhen_FaucetIsClosed() (gas: 16257)
TestDestroyTokens:test_RevertWhen_NotEnoughBalance() (gas: 29029)
TestGenerateTokens:testDeployment() (gas: 47719)
TestGenerateTokens:test_GenerateTokensAsOwner() (gas: 119330)
TestGenerateTokens:test_GenerateTokensOpenFaucet() (gas: 122001)
TestGenerateTokens:test_RevertWhen_FaucetIsClosed() (gas: 16311)
TestSNTV2:testDeployment() (gas: 47764)

6
.gitmodules vendored
View File

@ -2,3 +2,9 @@
branch = "v1" branch = "v1"
path = lib/forge-std path = lib/forge-std
url = https://github.com/foundry-rs/forge-std url = https://github.com/foundry-rs/forge-std
[submodule "lib/minime"]
path = lib/minime
url = https://github.com/vacp2p/minime
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts

103
README.md
View File

@ -1,4 +1,4 @@
# Foundry Template [![Github Actions][gha-badge]][gha] [![Foundry][foundry-badge]][foundry] [![License: MIT][license-badge]][license] # Status Network Token V2 [![Github Actions][gha-badge]][gha] [![Foundry][foundry-badge]][foundry] [![License: MIT][license-badge]][license]
[gha]: https://github.com/status-im/status-network-token-v2/actions [gha]: https://github.com/status-im/status-network-token-v2/actions
[gha-badge]: https://github.com/status-im/status-network-token-v2/actions/workflows/ci.yml/badge.svg [gha-badge]: https://github.com/status-im/status-network-token-v2/actions/workflows/ci.yml/badge.svg
@ -7,93 +7,9 @@
[license]: https://opensource.org/licenses/MIT [license]: https://opensource.org/licenses/MIT
[license-badge]: https://img.shields.io/badge/License-MIT-blue.svg [license-badge]: https://img.shields.io/badge/License-MIT-blue.svg
A Foundry-based template for developing Solidity smart contracts, with sensible defaults. This is the second iteration of [Status Network Token](https://github.com/status-im/status-network-token). The original
version of the token uses a lot of legacy code. This repository implements a more modern version based on our
This is a fork of [PaulRBerg's template](https://github.com/PaulRBerg/foundry-template) and adjusted to Vac's smart [MiniMeToken](https://github.com/vacp2p/minime) fork.
contracts unit's needs. See [Upstream differences](#upstream-differences) to learn more about how this template differs
from Paul's.
## What's Inside
- [Forge](https://github.com/foundry-rs/foundry/blob/master/forge): compile, test, fuzz, format, and deploy smart
contracts
- [Forge Std](https://github.com/foundry-rs/forge-std): collection of helpful contracts and cheatcodes for testing
- [Solhint Community](https://github.com/solhint-community/solhint-community): linter for Solidity code
## Getting Started
Click the [`Use this template`](https://github.com/vacp2p/foundry-template/generate) button at the top of the page to
create a new repository with this repo as the initial state.
Or, if you prefer to install the template manually:
```sh
$ mkdir my-project
$ cd my-project
$ forge init --template vacp2p/foundry-template
$ pnpm install # install Solhint, Prettier, and other Node.js deps
```
If this is your first time with Foundry, check out the
[installation](https://github.com/foundry-rs/foundry#installation) instructions.
## Features
This template builds upon the frameworks and libraries mentioned above, so for details about their specific features,
please consult their respective documentation.
For example, if you're interested in exploring Foundry in more detail, you should look at the
[Foundry Book](https://book.getfoundry.sh/). In particular, you may be interested in reading the
[Writing Tests](https://book.getfoundry.sh/forge/writing-tests.html) tutorial.
### Upstream differences
As mentioned above, this template is a fork with adjustments specific to the needs of Vac's smart contract service unit.
These differences are:
- **Removal of [PRBTest](https://github.com/PaulRBerg/prb-test)** - In an attempt to keep dependence on third-party code
low, we've decided to remove this library as a standard dependency of every project within Vac. If we do see a need
for it, we might bring it back in the future.
- **PROPERTIES.md** - For invariant testing and formal verification, we've introduced a `PROPERTIES.md` to document all
protocol properties that must hold true.
### Sensible Defaults
This template comes with a set of sensible default configurations for you to use. These defaults can be found in the
following files:
```text
├── .editorconfig
├── .gitignore
├── .prettierignore
├── .prettierrc.yml
├── .solhint.json
├── foundry.toml
├── remappings.txt
└── slither.config.json
```
### VSCode Integration
This template is IDE agnostic, but for the best user experience, you may want to use it in VSCode alongside Nomic
Foundation's [Solidity extension](https://marketplace.visualstudio.com/items?itemName=NomicFoundation.hardhat-solidity).
For guidance on how to integrate a Foundry project in VSCode, please refer to this
[guide](https://book.getfoundry.sh/config/vscode).
### GitHub Actions
This template comes with GitHub Actions pre-configured. Your contracts will be linted and tested on every push and pull
request made to the `main` branch.
You can edit the CI script in [.github/workflows/ci.yml](./.github/workflows/ci.yml).
## Writing Tests
If you would like to view the logs in the terminal output you can add the `-vvv` flag and use
[console.log](https://book.getfoundry.sh/faq?highlight=console.log#how-do-i-use-consolelog).
This template comes with an example test contract [Foo.t.sol](./test/Foo.t.sol)
## Usage ## Usage
@ -188,14 +104,3 @@ $ forge test
[guide](https://book.getfoundry.sh/projects/dependencies.html) in the book [guide](https://book.getfoundry.sh/projects/dependencies.html) in the book
2. You don't have to create a `.env` file, but filling in the environment variables may be useful when debugging and 2. You don't have to create a `.env` file, but filling in the environment variables may be useful when debugging and
testing against a fork. testing against a fork.
## Related Efforts
- [abigger87/femplate](https://github.com/abigger87/femplate)
- [cleanunicorn/ethereum-smartcontract-template](https://github.com/cleanunicorn/ethereum-smartcontract-template)
- [foundry-rs/forge-template](https://github.com/foundry-rs/forge-template)
- [FrankieIsLost/forge-template](https://github.com/FrankieIsLost/forge-template)
## License
This project is licensed under MIT.

1
lib/minime vendored Submodule

@ -0,0 +1 @@
Subproject commit 6d9d4f54876c2095f5aa79210869c8e17e30f55e

1
lib/openzeppelin-contracts vendored Submodule

@ -0,0 +1 @@
Subproject commit fd81a96f01cc42ef1c9a5399364968d0e07e9e90

View File

@ -1 +1,3 @@
forge-std/=lib/forge-std/src/ forge-std/=lib/forge-std/src/
@vacp2p/minime/contracts/=lib/minime/contracts/
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/

View File

@ -1,13 +1,24 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.19 <=0.9.0; pragma solidity >=0.8.19 <=0.9.0;
import { Foo } from "../src/Foo.sol";
import { BaseScript } from "./Base.s.sol"; import { BaseScript } from "./Base.s.sol";
import { DeploymentConfig } from "./DeploymentConfig.s.sol"; import { DeploymentConfig } from "./DeploymentConfig.s.sol";
import { SNTV2 } from "../src/SNTV2.sol";
import { SNTTokenController } from "../src/SNTTokenController.sol";
contract Deploy is BaseScript { contract Deploy is BaseScript {
function run() public returns (Foo foo, DeploymentConfig deploymentConfig) { function run() public returns (SNTV2, SNTTokenController, DeploymentConfig) {
deploymentConfig = new DeploymentConfig(broadcaster); DeploymentConfig deploymentConfig = new DeploymentConfig(broadcaster);
foo = new Foo(); (, string memory tokenName, string memory tokenSymbol, uint8 decimalUnits) =
deploymentConfig.activeNetworkConfig();
vm.startBroadcast(broadcaster);
SNTV2 sntV2 = new SNTV2(tokenName, decimalUnits, tokenSymbol);
SNTTokenController controller = new SNTTokenController(payable(address(sntV2)), false);
sntV2.changeController(payable(address(controller)));
vm.stopBroadcast();
return (sntV2, controller, deploymentConfig);
} }
} }

View File

@ -10,6 +10,9 @@ contract DeploymentConfig is Script {
struct NetworkConfig { struct NetworkConfig {
address deployer; address deployer;
string tokenName;
string tokenSymbol;
uint8 decimalUnits;
} }
NetworkConfig public activeNetworkConfig; NetworkConfig public activeNetworkConfig;
@ -27,7 +30,12 @@ contract DeploymentConfig is Script {
} }
function getOrCreateAnvilEthConfig() public view returns (NetworkConfig memory) { function getOrCreateAnvilEthConfig() public view returns (NetworkConfig memory) {
return NetworkConfig({ deployer: deployer }); return NetworkConfig({
deployer: deployer,
tokenName: "Status Network Token",
tokenSymbol: "SNT",
decimalUnits: 18
});
} }
// This function is a hack to have it excluded by `forge coverage` until // This function is a hack to have it excluded by `forge coverage` until

View File

@ -1,8 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.19;
contract Foo {
function id(uint256 value) external pure returns (uint256) {
return value;
}
}

74
src/SNTController.sol Normal file
View File

@ -0,0 +1,74 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
import { MiniMeBase } from "@vacp2p/minime/contracts/MiniMeBase.sol";
import { TokenController } from "@vacp2p/minime/contracts/TokenController.sol";
import { Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol";
/**
* @title SNTController
* @author r4bbit <r4bbit@status.im>
* @dev Controller contract for SNTV2 token.
* It's a no operation contract that can be replaced by controllers with more privileges.
*/
contract SNTController is TokenController, Ownable2Step {
MiniMeBase public snt;
/**
* @param _snt The address of the SNT token
*/
constructor(address payable _snt) {
snt = MiniMeBase(_snt);
}
/**
* @notice The owner of this contract can change the controller of the SNT token
* Please, be sure that the owner is a trusted agent or 0x0 address.
* @param _newController The address of the new controller
*/
function changeController(address payable _newController) public onlyOwner {
snt.changeController(_newController);
emit ControllerChanged(_newController);
}
/**
* @dev proxyPayment set to reject all Ether.
*/
function proxyPayment(address) public payable override returns (bool) {
return false;
}
/**
* @dev onTransfer set to accept any transfer
*/
function onTransfer(address, address, uint256) public pure override returns (bool) {
return true;
}
/**
* @dev onApprove set to accept any approval
*/
function onApprove(address, address, uint256) public pure override returns (bool) {
return true;
}
/**
* @notice Send tokens or ether from this contract to owner.
* @param _token Token contract to recover, 0 to extract ether.
*/
function claimTokens(MiniMeBase _token) public onlyOwner {
uint256 balance;
if (address(_token) == address(0)) {
balance = address(this).balance;
payable(msg.sender).transfer(balance);
return;
} else {
balance = _token.balanceOf(address(this));
_token.transfer(msg.sender, balance);
}
emit ClaimedTokens(address(_token), msg.sender, balance);
}
event ClaimedTokens(address indexed _token, address indexed _controller, uint256 _amount);
event ControllerChanged(address indexed _newController);
}

114
src/SNTTokenController.sol Normal file
View File

@ -0,0 +1,114 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
import { MiniMeBase } from "@vacp2p/minime/contracts/MiniMeBase.sol";
import { MiniMeToken } from "@vacp2p/minime/contracts/MiniMeToken.sol";
import { TokenController } from "@vacp2p/minime/contracts/TokenController.sol";
import { Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol";
/**
* @title SNTTokenController
* @author r4bbit <r4bbit@status.im>
* @dev Testnet controller contract for SNTV2 token.
* It's a no operation contract that allows for generating tokens.
* This is useful for test networks.
*/
contract SNTTokenController is TokenController, Ownable2Step {
error SNTTokenController_NotOpen();
MiniMeToken public snt;
bool public open;
/**
* @param _snt The address of the SNT token
*/
constructor(address payable _snt, bool _open) {
snt = MiniMeToken(_snt);
open = _open;
}
/**
* @notice The owner of this contract can set the open variable
* This is useful for test networks.
* @param _open The new value for the open variable
*/
function setOpen(bool _open) public onlyOwner {
open = _open;
}
/**
* @notice The owner of this contract can change the controller of the SNT token
* Please, be sure that the owner is a trusted agent or 0x0 address.
* @param _newController The address of the new controller
*/
function changeController(address payable _newController) public onlyOwner {
snt.changeController(_newController);
emit ControllerChanged(_newController);
}
/**
* @dev proxyPayment set to reject all Ether.
*/
function proxyPayment(address) public payable override returns (bool) {
return false;
}
/**
* @dev onTransfer set to accept any transfer
*/
function onTransfer(address, address, uint256) public pure override returns (bool) {
return true;
}
/**
* @dev onApprove set to accept any approval
*/
function onApprove(address, address, uint256) public pure override returns (bool) {
return true;
}
/**
* @notice Generate tokens for the given address.
* @param _to The address that will receive the minted tokens.
* @param _amount The amount of tokens to mint.
*/
function generateTokens(address _to, uint256 _amount) public {
if (!open && msg.sender != owner()) {
revert SNTTokenController_NotOpen();
}
snt.generateTokens(_to, _amount);
}
/**
* @notice Destroy tokens for the given address.
* @param _from The address that gets token removed.
* @param _amount The amount of tokens to destroy.
*/
function destroyTokens(address _from, uint256 _amount) public {
if (!open && msg.sender != owner()) {
revert SNTTokenController_NotOpen();
}
snt.destroyTokens(_from, _amount);
}
/**
* @notice Send tokens or ether from this contract to owner.
* @param _token Token contract to recover, 0 to extract ether.
*/
function claimTokens(MiniMeBase _token) public onlyOwner {
uint256 balance;
if (address(_token) == address(0)) {
balance = address(this).balance;
payable(msg.sender).transfer(balance);
return;
} else {
balance = _token.balanceOf(address(this));
_token.transfer(msg.sender, balance);
}
emit ClaimedTokens(address(_token), msg.sender, balance);
}
event ClaimedTokens(address indexed _token, address indexed _controller, uint256 _amount);
event ControllerChanged(address indexed _newController);
}

14
src/SNTV2.sol Normal file
View File

@ -0,0 +1,14 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.19;
import { MiniMeToken } from "@vacp2p/minime/contracts/MiniMeToken.sol";
contract SNTV2 is MiniMeToken {
constructor(
string memory _tokenName,
uint8 _decimalUnits,
string memory _tokenSymbol
)
MiniMeToken(MiniMeToken(payable(address(0))), 0, _tokenName, _decimalUnits, _tokenSymbol, true)
{ }
}

View File

@ -1,26 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.19 <0.9.0;
import { Test, console } from "forge-std/Test.sol";
import { Deploy } from "../script/Deploy.s.sol";
import { DeploymentConfig } from "../script/DeploymentConfig.s.sol";
import { Foo } from "../src/Foo.sol";
contract FooTest is Test {
Foo internal foo;
DeploymentConfig internal deploymentConfig;
address internal deployer;
function setUp() public virtual {
Deploy deployment = new Deploy();
(foo, deploymentConfig) = deployment.run();
}
function test_Example() external {
console.log("Hello World");
uint256 x = 42;
assertEq(foo.id(x), x, "value mismatch");
}
}

136
test/SNTV2.t.sol Normal file
View File

@ -0,0 +1,136 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.19;
import { Test } from "forge-std/Test.sol";
import { Deploy } from "../script/Deploy.s.sol";
import { DeploymentConfig } from "../script/DeploymentConfig.s.sol";
import { NotEnoughBalance } from "@vacp2p/minime/contracts/MiniMeBase.sol";
import { SNTV2 } from "../src/SNTV2.sol";
import { SNTTokenController } from "../src/SNTTokenController.sol";
contract TestSNTV2 is Test {
SNTV2 internal sntV2;
SNTTokenController internal controller;
DeploymentConfig internal deploymentConfig;
address internal deployer;
function setUp() public virtual {
Deploy deployment = new Deploy();
(sntV2, controller, deploymentConfig) = deployment.run();
(address _deployer,,,) = deploymentConfig.activeNetworkConfig();
deployer = _deployer;
}
function testDeployment() public {
(, string memory tokenName, string memory tokenSymbol, uint8 decimalUnits) =
deploymentConfig.activeNetworkConfig();
assertEq(sntV2.name(), tokenName);
assertEq(sntV2.symbol(), tokenSymbol);
assertEq(sntV2.decimals(), decimalUnits);
assertEq(sntV2.totalSupply(), 0);
assertEq(sntV2.controller(), address(controller));
assertEq(controller.owner(), deployer);
}
}
contract TestGenerateTokens is TestSNTV2 {
function setUp() public override {
TestSNTV2.setUp();
}
function test_RevertWhen_FaucetIsClosed() public {
// ensure faucet is closed
vm.prank(deployer);
controller.setOpen(false);
vm.expectRevert(SNTTokenController.SNTTokenController_NotOpen.selector);
controller.generateTokens(address(this), 100);
}
function test_GenerateTokensAsOwner() public {
vm.startPrank(deployer);
controller.setOpen(false);
uint256 balanceBefore = sntV2.balanceOf(address(this));
assertEq(balanceBefore, 0);
controller.generateTokens(address(this), 100);
uint256 balanceAfter = sntV2.balanceOf(address(this));
assertEq(balanceAfter, 100);
}
function test_GenerateTokensOpenFaucet() public {
// ensure faucet is open
vm.prank(deployer);
controller.setOpen(true);
uint256 balanceBefore = sntV2.balanceOf(address(this));
assertEq(balanceBefore, 0);
controller.generateTokens(address(this), 100);
uint256 balanceAfter = sntV2.balanceOf(address(this));
assertEq(balanceAfter, 100);
}
}
contract TestDestroyTokens is TestSNTV2 {
function setUp() public override {
TestSNTV2.setUp();
}
function test_RevertWhen_FaucetIsClosed() public {
// ensure faucet is closed
vm.prank(deployer);
controller.setOpen(false);
vm.expectRevert(SNTTokenController.SNTTokenController_NotOpen.selector);
controller.destroyTokens(address(this), 100);
}
function test_RevertWhen_NotEnoughBalance() public {
// ensure faucet is closed
vm.prank(deployer);
controller.setOpen(true);
vm.expectRevert(NotEnoughBalance.selector);
controller.destroyTokens(address(this), 100);
}
function test_DestroyTokensAsOwner() public {
// ensure faucet is open
vm.startPrank(deployer);
controller.setOpen(false);
uint256 balanceBefore = sntV2.balanceOf(address(this));
assertEq(balanceBefore, 0);
controller.generateTokens(address(this), 100);
uint256 balanceAfter = sntV2.balanceOf(address(this));
assertEq(balanceAfter, 100);
controller.destroyTokens(address(this), 100);
balanceAfter = sntV2.balanceOf(address(this));
assertEq(balanceAfter, 0);
}
function test_DestroyTokenFaucetIsOpen() public {
// ensure faucet is open
vm.prank(deployer);
controller.setOpen(true);
uint256 balanceBefore = sntV2.balanceOf(address(this));
assertEq(balanceBefore, 0);
controller.generateTokens(address(this), 100);
uint256 balanceAfter = sntV2.balanceOf(address(this));
assertEq(balanceAfter, 100);
controller.destroyTokens(address(this), 100);
balanceAfter = sntV2.balanceOf(address(this));
assertEq(balanceAfter, 0);
}
}