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 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/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/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 diff --git a/contracts/src/FinthetixRewardToken.sol b/contracts/src/FinthetixRewardToken.sol new file mode 100644 index 0000000..4ab1a50 --- /dev/null +++ b/contracts/src/FinthetixRewardToken.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +pragma solidity 0.8.23; + +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..a408df3 --- /dev/null +++ b/contracts/src/FinthetixStakingToken.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +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, FSTEvents { + 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); + emit SampleTokenRequested(msg.sender); + } +} diff --git a/contracts/test/FinthetixRewardToken.t.sol b/contracts/test/FinthetixRewardToken.t.sol new file mode 100644 index 0000000..a29cd5c --- /dev/null +++ b/contracts/test/FinthetixRewardToken.t.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +pragma solidity 0.8.23; + +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 private constant TOKEN_SYMBOL = "FRT"; + string private constant TOKEN_NAME = "FinthetixRewardToken"; + address private immutable contractOwnerAddr = vm.addr(0xB0b); + + FinthetixRewardToken private 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); + } +} diff --git a/contracts/test/FinthetixStakingToken.t.sol b/contracts/test/FinthetixStakingToken.t.sol new file mode 100644 index 0000000..81ebbda --- /dev/null +++ b/contracts/test/FinthetixStakingToken.t.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +pragma solidity 0.8.23; + +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 { + 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); + + vm.expectEmit(address(tokenContract)); + emit FSTEvents.SampleTokenRequested(address(this)); + tokenContract.requestSampleTokens(); + + uint256 tokensAfter = tokenContract.balanceOf(address(this)); + assertEq(tokensAfter, SAMPLE_TOKEN_QTY); + } +}