From 5629eb17a37cb6acd6b8ceabdeac6c830aedba2c Mon Sep 17 00:00:00 2001 From: todesstille <87335281+todesstille@users.noreply.github.com> Date: Fri, 19 Jul 2024 11:54:35 +0300 Subject: [PATCH] Feature/gov tokens (#216) * burnable pausable * capped * tokens * removed old tests --- contracts/gov/ERC20/ERC20GovBurnable.sol | 24 ++ contracts/gov/ERC20/ERC20GovCapped.sol | 49 +++ .../gov/ERC20/ERC20GovCappedPausable.sol | 67 ++++ contracts/gov/ERC20/ERC20GovMinimal.sol | 6 +- contracts/gov/ERC20/ERC20GovMintable.sol | 33 ++ .../gov/ERC20/ERC20GovMintablePausable.sol | 55 +++ contracts/gov/ERC20/ERC20GovPausable.sol | 44 +++ test/gov/ERC20/ERC20Gov.test.js | 24 -- test/gov/ERC20/ERC20GovTokens.test.js | 341 ++++++++++++++++++ 9 files changed, 616 insertions(+), 27 deletions(-) create mode 100644 contracts/gov/ERC20/ERC20GovBurnable.sol create mode 100644 contracts/gov/ERC20/ERC20GovCapped.sol create mode 100644 contracts/gov/ERC20/ERC20GovCappedPausable.sol create mode 100644 contracts/gov/ERC20/ERC20GovMintable.sol create mode 100644 contracts/gov/ERC20/ERC20GovMintablePausable.sol create mode 100644 contracts/gov/ERC20/ERC20GovPausable.sol create mode 100644 test/gov/ERC20/ERC20GovTokens.test.js diff --git a/contracts/gov/ERC20/ERC20GovBurnable.sol b/contracts/gov/ERC20/ERC20GovBurnable.sol new file mode 100644 index 00000000..5fed6b50 --- /dev/null +++ b/contracts/gov/ERC20/ERC20GovBurnable.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; + +contract ERC20GovBurnable is ERC20Upgradeable, ERC20BurnableUpgradeable { + struct InitMint { + address user; + uint256 amount; + } + + function __ERC20GovBurnable_init( + string calldata name, + string calldata symbol, + InitMint[] calldata distributions + ) external initializer { + __ERC20_init(name, symbol); + + for (uint256 i = 0; i < distributions.length; i++) { + _mint(distributions[i].user, distributions[i].amount); + } + } +} diff --git a/contracts/gov/ERC20/ERC20GovCapped.sol b/contracts/gov/ERC20/ERC20GovCapped.sol new file mode 100644 index 00000000..3497ef4d --- /dev/null +++ b/contracts/gov/ERC20/ERC20GovCapped.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20CappedUpgradeable.sol"; + +contract ERC20GovCapped is + ERC20Upgradeable, + OwnableUpgradeable, + ERC20BurnableUpgradeable, + ERC20CappedUpgradeable +{ + struct InitMint { + address user; + uint256 amount; + } + + function __ERC20GovCapped_init( + string calldata name, + string calldata symbol, + InitMint[] calldata distributions, + address newOwner, + uint256 cap_ + ) external initializer { + __ERC20_init(name, symbol); + + __Ownable_init(); + transferOwnership(newOwner); + + __ERC20Capped_init(cap_); + + for (uint256 i = 0; i < distributions.length; i++) { + _mint(distributions[i].user, distributions[i].amount); + } + } + + function mint(address account, uint256 amount) external onlyOwner { + _mint(account, amount); + } + + function _mint( + address account, + uint256 amount + ) internal virtual override(ERC20Upgradeable, ERC20CappedUpgradeable) { + ERC20CappedUpgradeable._mint(account, amount); + } +} diff --git a/contracts/gov/ERC20/ERC20GovCappedPausable.sol b/contracts/gov/ERC20/ERC20GovCappedPausable.sol new file mode 100644 index 00000000..f584eb87 --- /dev/null +++ b/contracts/gov/ERC20/ERC20GovCappedPausable.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20CappedUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol"; + +contract ERC20GovCappedPausable is + ERC20Upgradeable, + OwnableUpgradeable, + ERC20BurnableUpgradeable, + ERC20CappedUpgradeable, + ERC20PausableUpgradeable +{ + struct InitMint { + address user; + uint256 amount; + } + + function __ERC20GovCappedPausable_init( + string calldata name, + string calldata symbol, + InitMint[] calldata distributions, + address newOwner, + uint256 cap_ + ) external initializer { + __ERC20_init(name, symbol); + + __Ownable_init(); + transferOwnership(newOwner); + + __ERC20Capped_init(cap_); + + for (uint256 i = 0; i < distributions.length; i++) { + _mint(distributions[i].user, distributions[i].amount); + } + } + + function mint(address account, uint256 amount) external onlyOwner { + _mint(account, amount); + } + + function pause() external onlyOwner { + _pause(); + } + + function unpause() external onlyOwner { + _unpause(); + } + + function _mint( + address account, + uint256 amount + ) internal virtual override(ERC20Upgradeable, ERC20CappedUpgradeable) { + ERC20CappedUpgradeable._mint(account, amount); + } + + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal override(ERC20Upgradeable, ERC20PausableUpgradeable) { + ERC20PausableUpgradeable._beforeTokenTransfer(from, to, amount); + } +} diff --git a/contracts/gov/ERC20/ERC20GovMinimal.sol b/contracts/gov/ERC20/ERC20GovMinimal.sol index c8a72561..d0041f88 100644 --- a/contracts/gov/ERC20/ERC20GovMinimal.sol +++ b/contracts/gov/ERC20/ERC20GovMinimal.sol @@ -12,12 +12,12 @@ contract ERC20GovMinimal is ERC20Upgradeable { function __ERC20GovMinimal_init( string calldata name, string calldata symbol, - InitMint[] calldata initMint + InitMint[] calldata distributions ) external initializer { __ERC20_init(name, symbol); - for (uint256 i = 0; i < initMint.length; i++) { - _mint(initMint[i].user, initMint[i].amount); + for (uint256 i = 0; i < distributions.length; i++) { + _mint(distributions[i].user, distributions[i].amount); } } } diff --git a/contracts/gov/ERC20/ERC20GovMintable.sol b/contracts/gov/ERC20/ERC20GovMintable.sol new file mode 100644 index 00000000..0ea49c1d --- /dev/null +++ b/contracts/gov/ERC20/ERC20GovMintable.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +contract ERC20GovMintable is ERC20Upgradeable, ERC20BurnableUpgradeable, OwnableUpgradeable { + struct InitMint { + address user; + uint256 amount; + } + + function __ERC20GovMintable_init( + string calldata name, + string calldata symbol, + InitMint[] calldata distributions, + address newOwner + ) external initializer { + __ERC20_init(name, symbol); + + __Ownable_init(); + transferOwnership(newOwner); + + for (uint256 i = 0; i < distributions.length; i++) { + _mint(distributions[i].user, distributions[i].amount); + } + } + + function mint(address account, uint256 amount) external onlyOwner { + _mint(account, amount); + } +} diff --git a/contracts/gov/ERC20/ERC20GovMintablePausable.sol b/contracts/gov/ERC20/ERC20GovMintablePausable.sol new file mode 100644 index 00000000..bd9c0d4e --- /dev/null +++ b/contracts/gov/ERC20/ERC20GovMintablePausable.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol"; + +contract ERC20GovMintablePausable is + ERC20Upgradeable, + ERC20BurnableUpgradeable, + OwnableUpgradeable, + ERC20PausableUpgradeable +{ + struct InitMint { + address user; + uint256 amount; + } + + function __ERC20GovMintablePausable_init( + string calldata name, + string calldata symbol, + InitMint[] calldata distributions, + address newOwner + ) external initializer { + __ERC20_init(name, symbol); + + __Ownable_init(); + transferOwnership(newOwner); + + for (uint256 i = 0; i < distributions.length; i++) { + _mint(distributions[i].user, distributions[i].amount); + } + } + + function mint(address account, uint256 amount) external onlyOwner { + _mint(account, amount); + } + + function pause() external onlyOwner { + _pause(); + } + + function unpause() external onlyOwner { + _unpause(); + } + + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal override(ERC20Upgradeable, ERC20PausableUpgradeable) { + ERC20PausableUpgradeable._beforeTokenTransfer(from, to, amount); + } +} diff --git a/contracts/gov/ERC20/ERC20GovPausable.sol b/contracts/gov/ERC20/ERC20GovPausable.sol new file mode 100644 index 00000000..6914d2b3 --- /dev/null +++ b/contracts/gov/ERC20/ERC20GovPausable.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol"; + +contract ERC20GovPausable is ERC20Upgradeable, OwnableUpgradeable, ERC20PausableUpgradeable { + struct InitMint { + address user; + uint256 amount; + } + + function __ERC20GovPausable_init( + string calldata name, + string calldata symbol, + InitMint[] calldata distributions, + address newOwner + ) external initializer { + __Ownable_init(); + transferOwnership(newOwner); + __ERC20_init(name, symbol); + + for (uint256 i = 0; i < distributions.length; i++) { + _mint(distributions[i].user, distributions[i].amount); + } + } + + function pause() external onlyOwner { + _pause(); + } + + function unpause() external onlyOwner { + _unpause(); + } + + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal override(ERC20Upgradeable, ERC20PausableUpgradeable) { + ERC20PausableUpgradeable._beforeTokenTransfer(from, to, amount); + } +} diff --git a/test/gov/ERC20/ERC20Gov.test.js b/test/gov/ERC20/ERC20Gov.test.js index e323c424..d9c37051 100644 --- a/test/gov/ERC20/ERC20Gov.test.js +++ b/test/gov/ERC20/ERC20Gov.test.js @@ -5,10 +5,8 @@ const truffleAssert = require("truffle-assertions"); const Reverter = require("../../helpers/reverter"); const ERC20Gov = artifacts.require("ERC20Gov"); -const ERC20GovMinimal = artifacts.require("ERC20GovMinimal"); ERC20Gov.numberFormat = "BigNumber"; -ERC20GovMinimal.numberFormat = "BigNumber"; describe("ERC20Gov", () => { let OWNER; @@ -250,26 +248,4 @@ describe("ERC20Gov", () => { }); }); }); - - describe("ERC20GovMinimal", () => { - let erc20GovMinimal; - - beforeEach("", async () => { - erc20GovMinimal = await ERC20GovMinimal.new(); - await erc20GovMinimal.__ERC20GovMinimal_init("Token", "TKN", [[OWNER, wei("1")]]); - }); - - it("can't initialize twice", async () => { - await truffleAssert.reverts( - erc20GovMinimal.__ERC20GovMinimal_init("Token", "TKN", [[OWNER, wei("1")]]), - "Initializable: contract is already initialized", - ); - }); - - it("initializes correctly", async () => { - assert.equal(await erc20GovMinimal.name(), "Token"); - assert.equal(await erc20GovMinimal.symbol(), "TKN"); - assert.equal((await erc20GovMinimal.balanceOf(OWNER)).toFixed(), wei("1")); - }); - }); }); diff --git a/test/gov/ERC20/ERC20GovTokens.test.js b/test/gov/ERC20/ERC20GovTokens.test.js new file mode 100644 index 00000000..948cbaf9 --- /dev/null +++ b/test/gov/ERC20/ERC20GovTokens.test.js @@ -0,0 +1,341 @@ +const { assert } = require("chai"); +const { accounts, wei } = require("../../../scripts/utils/utils"); +const { ZERO_ADDR } = require("../../../scripts/utils/constants"); +const truffleAssert = require("truffle-assertions"); +const Reverter = require("../../helpers/reverter"); + +const ERC20GovMinimal = artifacts.require("ERC20GovMinimal"); +const ERC20GovBurnable = artifacts.require("ERC20GovBurnable"); +const ERC20GovPausable = artifacts.require("ERC20GovPausable"); +const ERC20GovMintable = artifacts.require("ERC20GovMintable"); +const ERC20GovCapped = artifacts.require("ERC20GovCapped"); +const ERC20GovMintablePausable = artifacts.require("ERC20GovMintablePausable"); +const ERC20GovCappedPausable = artifacts.require("ERC20GovCappedPausable"); + +ERC20GovMinimal.numberFormat = "BigNumber"; +ERC20GovBurnable.numberFormat = "BigNumber"; +ERC20GovPausable.numberFormat = "BigNumber"; +ERC20GovMintable.numberFormat = "BigNumber"; +ERC20GovCapped.numberFormat = "BigNumber"; +ERC20GovMintablePausable.numberFormat = "BigNumber"; +ERC20GovCappedPausable.numberFormat = "BigNumber"; + +const TOKEN_NAME = "Test token"; +const TOKEN_SYMBOL = "TST"; + +describe("ERC20Gov", () => { + let OWNER; + let SECOND; + let THIRD; + + const reverter = new Reverter(); + + before("setup", async () => { + OWNER = await accounts(0); + SECOND = await accounts(1); + THIRD = await accounts(2); + + erc20GovMinimal = await ERC20GovMinimal.new(); + erc20GovBurnable = await ERC20GovBurnable.new(); + erc20GovPausable = await ERC20GovPausable.new(); + erc20GovMintable = await ERC20GovMintable.new(); + erc20GovCapped = await ERC20GovCapped.new(); + erc20GovMintablePausable = await ERC20GovMintablePausable.new(); + erc20GovCappedPausable = await ERC20GovCappedPausable.new(); + + await erc20GovMinimal.__ERC20GovMinimal_init( + TOKEN_NAME, + TOKEN_SYMBOL, + [ + [SECOND, wei("2")], + [THIRD, wei("3")], + ], + { from: THIRD }, + ); + + await erc20GovBurnable.__ERC20GovBurnable_init( + TOKEN_NAME, + TOKEN_SYMBOL, + [ + [SECOND, wei("2")], + [THIRD, wei("3")], + ], + { from: THIRD }, + ); + + await erc20GovPausable.__ERC20GovPausable_init( + TOKEN_NAME, + TOKEN_SYMBOL, + [ + [SECOND, wei("2")], + [THIRD, wei("3")], + ], + OWNER, + { from: THIRD }, + ); + + await erc20GovMintable.__ERC20GovMintable_init( + TOKEN_NAME, + TOKEN_SYMBOL, + [ + [SECOND, wei("2")], + [THIRD, wei("3")], + ], + OWNER, + { from: THIRD }, + ); + + await erc20GovCapped.__ERC20GovCapped_init( + TOKEN_NAME, + TOKEN_SYMBOL, + [ + [SECOND, wei("2")], + [THIRD, wei("3")], + ], + OWNER, + wei("10"), + { from: THIRD }, + ); + + await erc20GovMintablePausable.__ERC20GovMintablePausable_init( + TOKEN_NAME, + TOKEN_SYMBOL, + [ + [SECOND, wei("2")], + [THIRD, wei("3")], + ], + OWNER, + { from: THIRD }, + ); + + await erc20GovCappedPausable.__ERC20GovCappedPausable_init( + TOKEN_NAME, + TOKEN_SYMBOL, + [ + [SECOND, wei("2")], + [THIRD, wei("3")], + ], + OWNER, + wei("10"), + { from: THIRD }, + ); + + INITIALIZABLE_LIST = [ + erc20GovMinimal, + erc20GovBurnable, + erc20GovPausable, + erc20GovMintable, + erc20GovCapped, + erc20GovMintablePausable, + erc20GovCappedPausable, + ]; + BURNABLE_LIST = [ + erc20GovBurnable, + erc20GovMintable, + erc20GovCapped, + erc20GovMintablePausable, + erc20GovCappedPausable, + ]; + OWNABLE_LIST = [ + erc20GovPausable, + erc20GovMintable, + erc20GovCapped, + erc20GovMintablePausable, + erc20GovCappedPausable, + ]; + PAUSABLE_LIST = [erc20GovPausable, erc20GovMintablePausable, erc20GovCappedPausable]; + MINTABLE_LIST = [erc20GovMintable, erc20GovCapped, erc20GovMintablePausable, erc20GovCappedPausable]; + CAPPED_LIST = [erc20GovCapped, erc20GovCappedPausable]; + MINTABLE_PAUSABLE_LIST = [erc20GovMintablePausable, erc20GovCappedPausable]; + + await reverter.snapshot(); + }); + + afterEach(reverter.revert); + + describe("Initialization", () => { + it("cant initialize twice", async () => { + await truffleAssert.reverts( + erc20GovMinimal.__ERC20GovMinimal_init(TOKEN_NAME, TOKEN_SYMBOL, [ + [SECOND, wei("2")], + [THIRD, wei("3")], + ]), + "Initializable: contract is already initialized", + ); + + await truffleAssert.reverts( + erc20GovBurnable.__ERC20GovBurnable_init(TOKEN_NAME, TOKEN_SYMBOL, [ + [SECOND, wei("2")], + [THIRD, wei("3")], + ]), + "Initializable: contract is already initialized", + ); + + await truffleAssert.reverts( + erc20GovPausable.__ERC20GovPausable_init( + TOKEN_NAME, + TOKEN_SYMBOL, + [ + [SECOND, wei("2")], + [THIRD, wei("3")], + ], + OWNER, + ), + "Initializable: contract is already initialized", + ); + + await truffleAssert.reverts( + erc20GovMintable.__ERC20GovMintable_init( + TOKEN_NAME, + TOKEN_SYMBOL, + [ + [SECOND, wei("2")], + [THIRD, wei("3")], + ], + OWNER, + ), + "Initializable: contract is already initialized", + ); + + await truffleAssert.reverts( + erc20GovCapped.__ERC20GovCapped_init( + TOKEN_NAME, + TOKEN_SYMBOL, + [ + [SECOND, wei("2")], + [THIRD, wei("3")], + ], + OWNER, + wei("10"), + ), + "Initializable: contract is already initialized", + ); + + await truffleAssert.reverts( + erc20GovMintablePausable.__ERC20GovMintablePausable_init( + TOKEN_NAME, + TOKEN_SYMBOL, + [ + [SECOND, wei("2")], + [THIRD, wei("3")], + ], + OWNER, + ), + "Initializable: contract is already initialized", + ); + + await truffleAssert.reverts( + erc20GovCappedPausable.__ERC20GovCappedPausable_init( + TOKEN_NAME, + TOKEN_SYMBOL, + [ + [SECOND, wei("2")], + [THIRD, wei("3")], + ], + OWNER, + wei("10"), + ), + "Initializable: contract is already initialized", + ); + }); + + it("correct initialization", async () => { + for (token of INITIALIZABLE_LIST) { + assert.equal(await token.name(), TOKEN_NAME); + assert.equal(await token.symbol(), TOKEN_SYMBOL); + assert.equal((await token.balanceOf(SECOND)).toFixed(), wei("2")); + assert.equal((await token.balanceOf(THIRD)).toFixed(), wei("3")); + assert.equal((await token.totalSupply()).toFixed(), wei("5")); + } + }); + }); + + describe("Burnable", () => { + it("could burn from itself", async () => { + for (token of BURNABLE_LIST) { + await token.burn(wei("1"), { from: SECOND }); + assert.equal((await token.balanceOf(SECOND)).toFixed(), wei("1")); + assert.equal((await token.balanceOf(THIRD)).toFixed(), wei("3")); + assert.equal((await token.totalSupply()).toFixed(), wei("4")); + } + }); + }); + + describe("Ownable", () => { + it("basic ownership properties", async () => { + for (token of OWNABLE_LIST) { + assert.equal(await token.owner(), OWNER); + + await token.transferOwnership(SECOND); + assert.equal(await token.owner(), SECOND); + + await token.renounceOwnership({ from: SECOND }); + assert.equal(await token.owner(), ZERO_ADDR); + } + }); + }); + + describe("Pausable", () => { + it("cant pause and unpause if not owner", async () => { + for (token of PAUSABLE_LIST) { + await truffleAssert.reverts(token.pause({ from: SECOND }), "Ownable: caller is not the owner"); + + await truffleAssert.reverts(token.unpause({ from: SECOND }), "Ownable: caller is not the owner"); + } + }); + + it("cant transfer when on pause", async () => { + for (token of PAUSABLE_LIST) { + await token.transfer(THIRD, 1, { from: SECOND }); + + await token.pause(); + + await truffleAssert.reverts( + token.transfer(THIRD, 1, { from: SECOND }), + "ERC20Pausable: token transfer while paused", + ); + + await token.unpause(); + + await token.transfer(THIRD, 1, { from: SECOND }); + } + }); + }); + + describe("Mintable", () => { + it("cant mint if not owner", async () => { + for (token of MINTABLE_LIST) { + await truffleAssert.reverts(token.mint(SECOND, wei("1"), { from: SECOND }), "Ownable: caller is not the owner"); + } + }); + + it("could mint if owner", async () => { + for (token of MINTABLE_LIST) { + assert.equal((await token.balanceOf(SECOND)).toFixed(), wei("2")); + + await token.mint(SECOND, wei("1")); + + assert.equal((await token.balanceOf(SECOND)).toFixed(), wei("3")); + } + }); + }); + + describe("Capped", () => { + it("couldnt mint over cap", async () => { + for (token of CAPPED_LIST) { + await truffleAssert.reverts(token.mint(SECOND, wei("20")), "ERC20Capped: cap exceeded"); + } + }); + }); + + describe("Mintable pausable", () => { + it("couldnt mint and burn during pause", async () => { + for (token of MINTABLE_PAUSABLE_LIST) { + await token.pause(); + + await truffleAssert.reverts(token.mint(SECOND, wei("1")), "ERC20Pausable: token transfer while paused"); + await truffleAssert.reverts(token.burn(SECOND, { from: SECOND }), "ERC20Pausable: token transfer while paused"); + } + }); + }); +});