From 44d7ec62c24f038737edcaff10ae726532558a3b Mon Sep 17 00:00:00 2001 From: Sayand Sathish Date: Sat, 30 Dec 2023 17:37:18 +0530 Subject: [PATCH 1/6] forge install: openzeppelin-contracts v5.0.1 --- .gitmodules | 3 +++ contracts/lib/openzeppelin-contracts | 1 + 2 files changed, 4 insertions(+) create mode 160000 contracts/lib/openzeppelin-contracts diff --git a/.gitmodules b/.gitmodules index c65a596..5c7d5d6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "contracts/lib/forge-std"] path = contracts/lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "contracts/lib/openzeppelin-contracts"] + path = contracts/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/contracts/lib/openzeppelin-contracts b/contracts/lib/openzeppelin-contracts new file mode 160000 index 0000000..01ef448 --- /dev/null +++ b/contracts/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit 01ef448981be9d20ca85f2faf6ebdf591ce409f3 From 48eccb18c8fa6c8c927a2cf0609ca63009a4b9c9 Mon Sep 17 00:00:00 2001 From: Sayand Sathish Date: Sat, 6 Jan 2024 09:17:46 +0530 Subject: [PATCH 2/6] contracts/feat: Add staking ERC20 token and associated test The contract has a special `requestSampleTokens` function which can be used to try the dapp for free. This eventually needs to be made gasless --- contracts/foundry.toml | 1 + contracts/src/StakingToken.sol | 12 ++++++++++++ contracts/test/StakingToken.t.sol | 30 ++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 contracts/src/StakingToken.sol create mode 100644 contracts/test/StakingToken.t.sol diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 25b918f..787df1a 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -2,5 +2,6 @@ src = "src" out = "out" libs = ["lib"] +remappings = ['@openzeppelin/contracts=lib/openzeppelin-contracts/contracts'] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/contracts/src/StakingToken.sol b/contracts/src/StakingToken.sol new file mode 100644 index 0000000..19a0457 --- /dev/null +++ b/contracts/src/StakingToken.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +pragma solidity 0.8.21; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract FinthetixStakingToken is ERC20 { + constructor() ERC20("FinthetixStakingToken", "FST") {} + + function requestSampleTokens() public { + _mint(msg.sender, 5 ether); + } +} diff --git a/contracts/test/StakingToken.t.sol b/contracts/test/StakingToken.t.sol new file mode 100644 index 0000000..3700afe --- /dev/null +++ b/contracts/test/StakingToken.t.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +pragma solidity 0.8.21; + +import {FinthetixStakingToken} from "src/StakingToken.sol"; +import {Test, console} from "forge-std/Test.sol"; + +contract StakingToken_UnitTest is Test { + string constant TOKEN_NAME = "FinthetixStakingToken"; + string constant TOKEN_SYMBOL = "FST"; + uint256 constant SAMPLE_TOKEN_QTY = 5 ether; + + FinthetixStakingToken tokenContract; + + function setUp() public { + tokenContract = new FinthetixStakingToken(); + } + + function test_HasCorrectLabels() public { + assertEq(tokenContract.name(), TOKEN_NAME); + assertEq(tokenContract.symbol(), TOKEN_SYMBOL); + } + + function test_RequestSampleTokensFn() public { + uint256 tokensBefore = tokenContract.balanceOf(address(this)); + assertEq(tokensBefore, 0); + tokenContract.requestSampleTokens(); + uint256 tokensAfter = tokenContract.balanceOf(address(this)); + assertEq(tokensAfter, SAMPLE_TOKEN_QTY); + } +} From 639deed1c4bd4da98497e6d1018e3422c072fcfb Mon Sep 17 00:00:00 2001 From: Sayand Sathish Date: Sat, 6 Jan 2024 12:19:36 +0530 Subject: [PATCH 3/6] contracts/feat: Add reward token and associated test --- contracts/src/RewardToken.sol | 13 +++++++++ contracts/test/RewardToken.t.sol | 47 ++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 contracts/src/RewardToken.sol create mode 100644 contracts/test/RewardToken.t.sol diff --git a/contracts/src/RewardToken.sol b/contracts/src/RewardToken.sol new file mode 100644 index 0000000..efaa02c --- /dev/null +++ b/contracts/src/RewardToken.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +pragma solidity 0.8.21; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +contract FinthetixRewardToken is ERC20, Ownable { + constructor() ERC20("FinthetixRewardToken", "FRT") Ownable(msg.sender) {} + + function mint(address to, uint256 amtToMint) external onlyOwner { + _mint(to, amtToMint); + } +} diff --git a/contracts/test/RewardToken.t.sol b/contracts/test/RewardToken.t.sol new file mode 100644 index 0000000..019e004 --- /dev/null +++ b/contracts/test/RewardToken.t.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +pragma solidity 0.8.21; + +import {FinthetixRewardToken} from "src/RewardToken.sol"; +import {Test} from "forge-std/Test.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +contract RewardToken_UnitTest is Test { + string constant TOKEN_SYMBOL = "FRT"; + string constant TOKEN_NAME = "FinthetixRewardToken"; + address immutable contractOwnerAddr = vm.addr(0xB0b); + + FinthetixRewardToken tokenContract; + + function setUp() public { + vm.prank(contractOwnerAddr); + tokenContract = new FinthetixRewardToken(); + } + + function test_HasCorrectLabels() public { + assertEq(tokenContract.symbol(), TOKEN_SYMBOL, "Incorrect Token Symbol"); + assertEq(tokenContract.name(), TOKEN_NAME, "Incorrect Token Name"); + } + + function test_CanMintToken(uint256 amtToMint) public { + address receiverAddr = address(1); + + uint256 balanceBefore = tokenContract.balanceOf(receiverAddr); + assertEq(balanceBefore, 0); + + vm.prank(contractOwnerAddr); + tokenContract.mint(receiverAddr, amtToMint); + + uint256 balanceAfter = tokenContract.balanceOf(receiverAddr); + assertEq(balanceAfter, amtToMint); + } + + function test_NonOwnersCannotMint(address senderAddr) public { + vm.assume(senderAddr != contractOwnerAddr); + + uint8 amtToMint = 2; + + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, senderAddr)); + vm.prank(senderAddr); + tokenContract.mint(senderAddr, amtToMint); + } +} From b39f6b4aff5caa7bfab606f7a4cf3f280c6a34a4 Mon Sep 17 00:00:00 2001 From: Sayand Sathish Date: Sat, 6 Jan 2024 14:08:36 +0530 Subject: [PATCH 4/6] contracts/docs: Add natspec documentation for staking and reward tokens Some minor style changes were also made to make the visibility of storage variables more explicit --- contracts/src/FinthetixRewardToken.sol | 28 +++++++++++++++++++ contracts/src/FinthetixStakingToken.sol | 22 +++++++++++++++ contracts/src/RewardToken.sol | 13 --------- contracts/src/StakingToken.sol | 12 -------- ...Token.t.sol => FinthetixRewardToken.t.sol} | 10 +++---- ...oken.t.sol => FinthetixStakingToken.t.sol} | 12 ++++---- 6 files changed, 61 insertions(+), 36 deletions(-) create mode 100644 contracts/src/FinthetixRewardToken.sol create mode 100644 contracts/src/FinthetixStakingToken.sol delete mode 100644 contracts/src/RewardToken.sol delete mode 100644 contracts/src/StakingToken.sol rename contracts/test/{RewardToken.t.sol => FinthetixRewardToken.t.sol} (81%) rename contracts/test/{StakingToken.t.sol => FinthetixStakingToken.t.sol} (67%) diff --git a/contracts/src/FinthetixRewardToken.sol b/contracts/src/FinthetixRewardToken.sol new file mode 100644 index 0000000..aa0f98d --- /dev/null +++ b/contracts/src/FinthetixRewardToken.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +pragma solidity 0.8.21; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +/** + * @title FinthetixRewardtoken + * @author sayandcode + * @notice This token is paid out by the Finthetix Staking Contract as a reward + * @dev This contract needs to be deployed by the Staking Contract in order for it to be able to + * generate rewards periodically. This is because only the owner of this Token Contract can mint + * more tokens + */ +contract FinthetixRewardToken is ERC20, Ownable { + constructor() ERC20("FinthetixRewardToken", "FRT") Ownable(msg.sender) {} + + /** + * + * @param to The address of the recipient of the funds + * @param amtToMint The amount of tokens to allocate to the recipient + * @notice This function will be called by the staking contract when it is time to generate rewards for stakers + * @dev This is intended to be only used by the Staking Contract + */ + function mint(address to, uint256 amtToMint) external onlyOwner { + _mint(to, amtToMint); + } +} diff --git a/contracts/src/FinthetixStakingToken.sol b/contracts/src/FinthetixStakingToken.sol new file mode 100644 index 0000000..2cb0c28 --- /dev/null +++ b/contracts/src/FinthetixStakingToken.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +pragma solidity 0.8.21; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/** + * @title FinthetixStakingToken + * @author sayandcode + * @notice This is the staking token used by the Finthetix Staking Contract. Users lock up this token + * to gain rewards in the token chosen by the staking contract. + */ +contract FinthetixStakingToken is ERC20 { + constructor() ERC20("FinthetixStakingToken", "FST") {} + + /** + * @notice Users can call this function to get a few sample tokens, in order to try out the + * staking contract + */ + function requestSampleTokens() public { + _mint(msg.sender, 5 ether); + } +} diff --git a/contracts/src/RewardToken.sol b/contracts/src/RewardToken.sol deleted file mode 100644 index efaa02c..0000000 --- a/contracts/src/RewardToken.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: SEE LICENSE IN LICENSE -pragma solidity 0.8.21; - -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; - -contract FinthetixRewardToken is ERC20, Ownable { - constructor() ERC20("FinthetixRewardToken", "FRT") Ownable(msg.sender) {} - - function mint(address to, uint256 amtToMint) external onlyOwner { - _mint(to, amtToMint); - } -} diff --git a/contracts/src/StakingToken.sol b/contracts/src/StakingToken.sol deleted file mode 100644 index 19a0457..0000000 --- a/contracts/src/StakingToken.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: SEE LICENSE IN LICENSE -pragma solidity 0.8.21; - -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -contract FinthetixStakingToken is ERC20 { - constructor() ERC20("FinthetixStakingToken", "FST") {} - - function requestSampleTokens() public { - _mint(msg.sender, 5 ether); - } -} diff --git a/contracts/test/RewardToken.t.sol b/contracts/test/FinthetixRewardToken.t.sol similarity index 81% rename from contracts/test/RewardToken.t.sol rename to contracts/test/FinthetixRewardToken.t.sol index 019e004..97cf9f6 100644 --- a/contracts/test/RewardToken.t.sol +++ b/contracts/test/FinthetixRewardToken.t.sol @@ -1,16 +1,16 @@ // SPDX-License-Identifier: SEE LICENSE IN LICENSE pragma solidity 0.8.21; -import {FinthetixRewardToken} from "src/RewardToken.sol"; +import {FinthetixRewardToken} from "src/FinthetixRewardToken.sol"; import {Test} from "forge-std/Test.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; contract RewardToken_UnitTest is Test { - string constant TOKEN_SYMBOL = "FRT"; - string constant TOKEN_NAME = "FinthetixRewardToken"; - address immutable contractOwnerAddr = vm.addr(0xB0b); + string private constant TOKEN_SYMBOL = "FRT"; + string private constant TOKEN_NAME = "FinthetixRewardToken"; + address private immutable contractOwnerAddr = vm.addr(0xB0b); - FinthetixRewardToken tokenContract; + FinthetixRewardToken private tokenContract; function setUp() public { vm.prank(contractOwnerAddr); diff --git a/contracts/test/StakingToken.t.sol b/contracts/test/FinthetixStakingToken.t.sol similarity index 67% rename from contracts/test/StakingToken.t.sol rename to contracts/test/FinthetixStakingToken.t.sol index 3700afe..c1e21d4 100644 --- a/contracts/test/StakingToken.t.sol +++ b/contracts/test/FinthetixStakingToken.t.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: SEE LICENSE IN LICENSE pragma solidity 0.8.21; -import {FinthetixStakingToken} from "src/StakingToken.sol"; -import {Test, console} from "forge-std/Test.sol"; +import {FinthetixStakingToken} from "src/FinthetixStakingToken.sol"; +import {Test} from "forge-std/Test.sol"; contract StakingToken_UnitTest is Test { - string constant TOKEN_NAME = "FinthetixStakingToken"; - string constant TOKEN_SYMBOL = "FST"; - uint256 constant SAMPLE_TOKEN_QTY = 5 ether; + string private constant TOKEN_NAME = "FinthetixStakingToken"; + string private constant TOKEN_SYMBOL = "FST"; + uint256 private constant SAMPLE_TOKEN_QTY = 5 ether; - FinthetixStakingToken tokenContract; + FinthetixStakingToken private tokenContract; function setUp() public { tokenContract = new FinthetixStakingToken(); From 2aad57c17686f6558057388a2f889759683639c6 Mon Sep 17 00:00:00 2001 From: Sayand Sathish Date: Sat, 6 Jan 2024 14:44:40 +0530 Subject: [PATCH 5/6] contracts/feat: Add SampleTokenRequested event to FST token contract This event is emitted when the user requests a sample of FST tokens. In order to get around the 0.8.21 bug regarding usage of event from interfaces, we also had to update all contracts to 0.8.23 --- contracts/src/FinthetixRewardToken.sol | 2 +- contracts/src/FinthetixStakingToken.sol | 14 ++++++++++++-- contracts/test/FinthetixRewardToken.t.sol | 2 +- contracts/test/FinthetixStakingToken.t.sol | 9 ++++++--- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/contracts/src/FinthetixRewardToken.sol b/contracts/src/FinthetixRewardToken.sol index aa0f98d..4ab1a50 100644 --- a/contracts/src/FinthetixRewardToken.sol +++ b/contracts/src/FinthetixRewardToken.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: SEE LICENSE IN LICENSE -pragma solidity 0.8.21; +pragma solidity 0.8.23; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; diff --git a/contracts/src/FinthetixStakingToken.sol b/contracts/src/FinthetixStakingToken.sol index 2cb0c28..a408df3 100644 --- a/contracts/src/FinthetixStakingToken.sol +++ b/contracts/src/FinthetixStakingToken.sol @@ -1,15 +1,24 @@ // SPDX-License-Identifier: SEE LICENSE IN LICENSE -pragma solidity 0.8.21; +pragma solidity 0.8.23; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +interface FSTEvents { + /** + * + * @param requesterAddr The address of the user requesting a sample of FST tokens + * @dev This event is emitted when the user requests a sample of FST tokens + */ + event SampleTokenRequested(address requesterAddr); +} + /** * @title FinthetixStakingToken * @author sayandcode * @notice This is the staking token used by the Finthetix Staking Contract. Users lock up this token * to gain rewards in the token chosen by the staking contract. */ -contract FinthetixStakingToken is ERC20 { +contract FinthetixStakingToken is ERC20, FSTEvents { constructor() ERC20("FinthetixStakingToken", "FST") {} /** @@ -18,5 +27,6 @@ contract FinthetixStakingToken is ERC20 { */ function requestSampleTokens() public { _mint(msg.sender, 5 ether); + emit SampleTokenRequested(msg.sender); } } diff --git a/contracts/test/FinthetixRewardToken.t.sol b/contracts/test/FinthetixRewardToken.t.sol index 97cf9f6..a29cd5c 100644 --- a/contracts/test/FinthetixRewardToken.t.sol +++ b/contracts/test/FinthetixRewardToken.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: SEE LICENSE IN LICENSE -pragma solidity 0.8.21; +pragma solidity 0.8.23; import {FinthetixRewardToken} from "src/FinthetixRewardToken.sol"; import {Test} from "forge-std/Test.sol"; diff --git a/contracts/test/FinthetixStakingToken.t.sol b/contracts/test/FinthetixStakingToken.t.sol index c1e21d4..81ebbda 100644 --- a/contracts/test/FinthetixStakingToken.t.sol +++ b/contracts/test/FinthetixStakingToken.t.sol @@ -1,14 +1,13 @@ // SPDX-License-Identifier: SEE LICENSE IN LICENSE -pragma solidity 0.8.21; +pragma solidity 0.8.23; -import {FinthetixStakingToken} from "src/FinthetixStakingToken.sol"; +import {FinthetixStakingToken, FSTEvents} from "src/FinthetixStakingToken.sol"; import {Test} from "forge-std/Test.sol"; contract StakingToken_UnitTest is Test { string private constant TOKEN_NAME = "FinthetixStakingToken"; string private constant TOKEN_SYMBOL = "FST"; uint256 private constant SAMPLE_TOKEN_QTY = 5 ether; - FinthetixStakingToken private tokenContract; function setUp() public { @@ -23,7 +22,11 @@ contract StakingToken_UnitTest is Test { function test_RequestSampleTokensFn() public { uint256 tokensBefore = tokenContract.balanceOf(address(this)); assertEq(tokensBefore, 0); + + vm.expectEmit(address(tokenContract)); + emit FSTEvents.SampleTokenRequested(address(this)); tokenContract.requestSampleTokens(); + uint256 tokensAfter = tokenContract.balanceOf(address(this)); assertEq(tokensAfter, SAMPLE_TOKEN_QTY); } From 433e07337a14304545633a577f5db1321aa32539 Mon Sep 17 00:00:00 2001 From: Sayand Sathish Date: Sun, 7 Jan 2024 15:48:25 +0530 Subject: [PATCH 6/6] ci: Update contracts testing workflow to run automatically on PR open --- .github/workflows/test-contracts.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-contracts.yml b/.github/workflows/test-contracts.yml index 0ef9a15..f69d3d5 100644 --- a/.github/workflows/test-contracts.yml +++ b/.github/workflows/test-contracts.yml @@ -1,6 +1,12 @@ name: Test Contracts -on: workflow_dispatch +on: + pull_request: + types: + - opened + - synchronize + - reopened + - edited env: FOUNDRY_PROFILE: ci