From f32a4a99e60d7187b27b1f762c6b3f10c0a35d74 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 18 Aug 2021 19:22:18 +1000 Subject: [PATCH 01/10] chore: gas optimization on sig verifications --- contracts/governance/staking/deps/SignatureVerifier.sol | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/contracts/governance/staking/deps/SignatureVerifier.sol b/contracts/governance/staking/deps/SignatureVerifier.sol index 303dc2c7..dbf4f222 100644 --- a/contracts/governance/staking/deps/SignatureVerifier.sol +++ b/contracts/governance/staking/deps/SignatureVerifier.sol @@ -25,7 +25,6 @@ pragma solidity ^0.8.0; library SignatureVerifier { - function verify( address signer, address account, @@ -38,16 +37,16 @@ library SignatureVerifier { return recoverSigner(ethSignedMessageHash, signature) == signer; } - function getMessageHash(address account, uint256 id) public pure returns (bytes32) { + function getMessageHash(address account, uint256 id) internal pure returns (bytes32) { return keccak256(abi.encodePacked(account, id)); } - function getEthSignedMessageHash(bytes32 messageHash) public pure returns (bytes32) { + function getEthSignedMessageHash(bytes32 messageHash) internal pure returns (bytes32) { return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); } function recoverSigner(bytes32 _ethSignedMessageHash, bytes memory _signature) - public + internal pure returns (address) { From f8ce36015cef2127e4cceec58f5c8aad36bb3387 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 18 Aug 2021 21:37:14 +1000 Subject: [PATCH 02/10] chore: linter --- .../staking/PlatformTokenVendorFactory.sol | 3 +- test/governance/staked-token.spec.ts | 149 +++++------------- 2 files changed, 38 insertions(+), 114 deletions(-) diff --git a/contracts/rewards/staking/PlatformTokenVendorFactory.sol b/contracts/rewards/staking/PlatformTokenVendorFactory.sol index a3997c97..7c10f7a0 100644 --- a/contracts/rewards/staking/PlatformTokenVendorFactory.sol +++ b/contracts/rewards/staking/PlatformTokenVendorFactory.sol @@ -1,4 +1,3 @@ - // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity 0.8.6; @@ -17,7 +16,7 @@ library PlatformTokenVendorFactory { return true; } - /** + /** * @notice Deploys a new PlatformTokenVendor contract * @param _rewardsToken reward or platform rewards token. eg MTA or WMATIC * @return address of the deployed PlatformTokenVendor contract diff --git a/test/governance/staked-token.spec.ts b/test/governance/staked-token.spec.ts index c23602f5..ddb08e0c 100644 --- a/test/governance/staked-token.spec.ts +++ b/test/governance/staked-token.spec.ts @@ -23,7 +23,6 @@ import { getTimestamp, increaseTime } from "@utils/time" import { arrayify, solidityKeccak256 } from "ethers/lib/utils" import { BigNumberish, Signer } from "ethers" import { QuestStatus, QuestType, UserStakingData } from "types/stakedToken" -import { usdFormatter } from "tasks/utils" const signUserQuest = async (user: string, questId: BigNumberish, questSigner: Signer): Promise => { const messageHash = solidityKeccak256(["address", "uint256"], [user, questId]) @@ -148,16 +147,10 @@ describe("Staked Token", () => { const stakedTimestamp = await getTimestamp() - await expect(tx) - .to.emit(stakedToken, "Staked") - .withArgs(sa.default.address, stakedAmount, ZERO_ADDRESS) + await expect(tx).to.emit(stakedToken, "Staked").withArgs(sa.default.address, stakedAmount, ZERO_ADDRESS) await expect(tx).to.emit(stakedToken, "DelegateChanged").not - await expect(tx) - .to.emit(stakedToken, "DelegateVotesChanged") - .withArgs(sa.default.address, 0, stakedAmount) - await expect(tx) - .to.emit(rewardToken, "Transfer") - .withArgs(sa.default.address, stakedToken.address, stakedAmount) + await expect(tx).to.emit(stakedToken, "DelegateVotesChanged").withArgs(sa.default.address, 0, stakedAmount) + await expect(tx).to.emit(rewardToken, "Transfer").withArgs(sa.default.address, stakedToken.address, stakedAmount) const afterData = await snapshotUserStakingData(sa.default.address) @@ -180,18 +173,10 @@ describe("Staked Token", () => { const stakedTimestamp = await getTimestamp() - await expect(tx) - .to.emit(stakedToken, "Staked") - .withArgs(sa.default.address, stakedAmount, sa.dummy1.address) - await expect(tx) - .to.emit(stakedToken, "DelegateChanged") - .withArgs(sa.default.address, sa.default.address, sa.dummy1.address) - await expect(tx) - .to.emit(stakedToken, "DelegateVotesChanged") - .withArgs(sa.dummy1.address, 0, stakedAmount) - await expect(tx) - .to.emit(rewardToken, "Transfer") - .withArgs(sa.default.address, stakedToken.address, stakedAmount) + await expect(tx).to.emit(stakedToken, "Staked").withArgs(sa.default.address, stakedAmount, sa.dummy1.address) + await expect(tx).to.emit(stakedToken, "DelegateChanged").withArgs(sa.default.address, sa.default.address, sa.dummy1.address) + await expect(tx).to.emit(stakedToken, "DelegateVotesChanged").withArgs(sa.dummy1.address, 0, stakedAmount) + await expect(tx).to.emit(rewardToken, "Transfer").withArgs(sa.default.address, stakedToken.address, stakedAmount) const stakerDataAfter = await snapshotUserStakingData(sa.default.address) expect(stakerDataAfter.userBalances.raw, "staker raw balance after").to.eq(stakedAmount) @@ -256,15 +241,9 @@ describe("Staked Token", () => { const tx = await stakedToken.delegate(sa.dummy1.address) // Events from delegate tx - await expect(tx) - .to.emit(stakedToken, "DelegateChanged") - .withArgs(sa.default.address, sa.default.address, sa.dummy1.address) - await expect(tx) - .to.emit(stakedToken, "DelegateVotesChanged") - .withArgs(sa.default.address, stakedAmount, 0) - await expect(tx) - .to.emit(stakedToken, "DelegateVotesChanged") - .withArgs(sa.dummy1.address, 0, stakedAmount) + await expect(tx).to.emit(stakedToken, "DelegateChanged").withArgs(sa.default.address, sa.default.address, sa.dummy1.address) + await expect(tx).to.emit(stakedToken, "DelegateVotesChanged").withArgs(sa.default.address, stakedAmount, 0) + await expect(tx).to.emit(stakedToken, "DelegateVotesChanged").withArgs(sa.dummy1.address, 0, stakedAmount) // Staker const stakerDataAfter = await snapshotUserStakingData(sa.default.address) @@ -292,15 +271,9 @@ describe("Staked Token", () => { expect(newDelegateDataBefore.votes).to.equal(0) const tx = await stakedToken.delegate(sa.dummy2.address) - await expect(tx) - .to.emit(stakedToken, "DelegateChanged") - .withArgs(sa.default.address, sa.dummy1.address, sa.dummy2.address) - await expect(tx) - .to.emit(stakedToken, "DelegateVotesChanged") - .withArgs(sa.dummy1.address, stakedAmount, 0) - await expect(tx) - .to.emit(stakedToken, "DelegateVotesChanged") - .withArgs(sa.dummy2.address, 0, stakedAmount) + await expect(tx).to.emit(stakedToken, "DelegateChanged").withArgs(sa.default.address, sa.dummy1.address, sa.dummy2.address) + await expect(tx).to.emit(stakedToken, "DelegateVotesChanged").withArgs(sa.dummy1.address, stakedAmount, 0) + await expect(tx).to.emit(stakedToken, "DelegateVotesChanged").withArgs(sa.dummy2.address, 0, stakedAmount) const stakerDataAfter = await snapshotUserStakingData(sa.default.address) expect(stakerDataAfter.votes).to.equal(0) @@ -326,15 +299,9 @@ describe("Staked Token", () => { const tx = await stakedToken.delegate(sa.default.address) // Events - await expect(tx) - .to.emit(stakedToken, "DelegateChanged") - .withArgs(sa.default.address, sa.dummy1.address, sa.default.address) - await expect(tx) - .to.emit(stakedToken, "DelegateVotesChanged") - .withArgs(sa.default.address, 0, stakedAmount) - await expect(tx) - .to.emit(stakedToken, "DelegateVotesChanged") - .withArgs(sa.dummy1.address, stakedAmount, 0) + await expect(tx).to.emit(stakedToken, "DelegateChanged").withArgs(sa.default.address, sa.dummy1.address, sa.default.address) + await expect(tx).to.emit(stakedToken, "DelegateVotesChanged").withArgs(sa.default.address, 0, stakedAmount) + await expect(tx).to.emit(stakedToken, "DelegateVotesChanged").withArgs(sa.dummy1.address, stakedAmount, 0) // Staker const stakerDataAfter = await snapshotUserStakingData(sa.default.address) @@ -350,9 +317,7 @@ describe("Staked Token", () => { }) it("by delegate", async () => { const tx = await stakedToken.connect(sa.dummy1.signer).delegate(sa.dummy2.address) - await expect(tx) - .to.emit(stakedToken, "DelegateChanged") - .withArgs(sa.dummy1.address, sa.dummy1.address, sa.dummy2.address) + await expect(tx).to.emit(stakedToken, "DelegateChanged").withArgs(sa.dummy1.address, sa.dummy1.address, sa.dummy2.address) }) context("should fail", () => { it("by delegate", async () => { @@ -455,9 +420,7 @@ describe("Staked Token", () => { const currentTime = await getTimestamp() const tx = await stakedToken.connect(sa.governor.signer).expireQuest(id) - await expect(tx) - .to.emit(stakedToken, "QuestExpired") - .withArgs(id) + await expect(tx).to.emit(stakedToken, "QuestExpired").withArgs(id) const quest = await stakedToken.getQuest(id) expect(quest.status).to.eq(QuestStatus.EXPIRED) @@ -471,9 +434,7 @@ describe("Staked Token", () => { const currentTime = await getTimestamp() const tx = await stakedToken.connect(sa.governor.signer).expireQuest(id) - await expect(tx) - .to.emit(stakedToken, "QuestExpired") - .withArgs(id) + await expect(tx).to.emit(stakedToken, "QuestExpired").withArgs(id) const quest = await stakedToken.getQuest(id) expect(quest.status).to.eq(QuestStatus.EXPIRED) @@ -554,9 +515,7 @@ describe("Staked Token", () => { const completeQuestTimestamp = await getTimestamp() // Check events - await expect(tx) - .to.emit(stakedToken, "QuestComplete") - .withArgs(userAddress, seasonQuestId) + await expect(tx).to.emit(stakedToken, "QuestComplete").withArgs(userAddress, seasonQuestId) // Check data expect(await stakedToken.hasCompleted(userAddress, seasonQuestId), "quest completed after").to.be.true @@ -585,9 +544,7 @@ describe("Staked Token", () => { const completeQuestTimestamp = await getTimestamp() // Check events - await expect(tx) - .to.emit(stakedToken, "QuestComplete") - .withArgs(userAddress, permanentQuestId) + await expect(tx).to.emit(stakedToken, "QuestComplete").withArgs(userAddress, permanentQuestId) // Check data expect(await stakedToken.hasCompleted(userAddress, permanentQuestId), "quest completed after").to.be.true @@ -619,9 +576,7 @@ describe("Staked Token", () => { const completeQuestTimestamp = await getTimestamp() // Check events - await expect(tx) - .to.emit(stakedToken, "QuestComplete") - .withArgs(userAddress, permanentQuestId) + await expect(tx).to.emit(stakedToken, "QuestComplete").withArgs(userAddress, permanentQuestId) // Check data expect(await stakedToken.hasCompleted(userAddress, permanentQuestId), "quest completed after").to.be.true @@ -957,9 +912,7 @@ describe("Staked Token", () => { it("should start cooldown", async () => { const tx = await stakedToken.startCooldown(cooldown100Percentage) - await expect(tx) - .to.emit(stakedToken, "Cooldown") - .withArgs(sa.default.address, cooldown100Percentage) + await expect(tx).to.emit(stakedToken, "Cooldown").withArgs(sa.default.address, cooldown100Percentage) const startCooldownTimestamp = await getTimestamp() const stakerDataAfter = await snapshotUserStakingData(sa.default.address) @@ -1029,9 +982,7 @@ describe("Staked Token", () => { await increaseTime(ONE_DAY) const tx = await stakedToken.endCooldown() - await expect(tx) - .to.emit(stakedToken, "CooldownExited") - .withArgs(sa.default.address) + await expect(tx).to.emit(stakedToken, "CooldownExited").withArgs(sa.default.address) const endCooldownTimestamp = await getTimestamp() const stakerDataAfter = await snapshotUserStakingData(sa.default.address) @@ -1051,9 +1002,7 @@ describe("Staked Token", () => { await increaseTime(ONE_DAY.mul(8)) const tx = await stakedToken.endCooldown() - await expect(tx) - .to.emit(stakedToken, "CooldownExited") - .withArgs(sa.default.address) + await expect(tx).to.emit(stakedToken, "CooldownExited").withArgs(sa.default.address) const endCooldownTimestamp = await getTimestamp() const stakerDataAfter = await snapshotUserStakingData(sa.default.address) @@ -1073,9 +1022,7 @@ describe("Staked Token", () => { await increaseTime(ONE_DAY.mul(12)) const tx = await stakedToken.endCooldown() - await expect(tx) - .to.emit(stakedToken, "CooldownExited") - .withArgs(sa.default.address) + await expect(tx).to.emit(stakedToken, "CooldownExited").withArgs(sa.default.address) const endCooldownTimestamp = await getTimestamp() const stakerDataAfter = await snapshotUserStakingData(sa.default.address) @@ -1095,9 +1042,7 @@ describe("Staked Token", () => { await increaseTime(ONE_WEEK.mul(14)) const tx = await stakedToken.endCooldown() - await expect(tx) - .to.emit(stakedToken, "CooldownExited") - .withArgs(sa.default.address) + await expect(tx).to.emit(stakedToken, "CooldownExited").withArgs(sa.default.address) const endCooldownTimestamp = await getTimestamp() const stakerDataAfter = await snapshotUserStakingData(sa.default.address) @@ -1123,9 +1068,7 @@ describe("Staked Token", () => { await increaseTime(ONE_DAY) const tx = await stakedToken.endCooldown() - await expect(tx) - .to.emit(stakedToken, "CooldownExited") - .withArgs(sa.default.address) + await expect(tx).to.emit(stakedToken, "CooldownExited").withArgs(sa.default.address) const endCooldownTimestamp = await getTimestamp() const stakerDataAfter = await snapshotUserStakingData(sa.default.address) @@ -1145,9 +1088,7 @@ describe("Staked Token", () => { await increaseTime(ONE_DAY.mul(8)) const tx = await stakedToken.endCooldown() - await expect(tx) - .to.emit(stakedToken, "CooldownExited") - .withArgs(sa.default.address) + await expect(tx).to.emit(stakedToken, "CooldownExited").withArgs(sa.default.address) const endCooldownTimestamp = await getTimestamp() const stakerDataAfter = await snapshotUserStakingData(sa.default.address) @@ -1167,9 +1108,7 @@ describe("Staked Token", () => { await increaseTime(ONE_DAY.mul(12)) const tx = await stakedToken.endCooldown() - await expect(tx) - .to.emit(stakedToken, "CooldownExited") - .withArgs(sa.default.address) + await expect(tx).to.emit(stakedToken, "CooldownExited").withArgs(sa.default.address) const endCooldownTimestamp = await getTimestamp() const stakerDataAfter = await snapshotUserStakingData(sa.default.address) @@ -1216,19 +1155,13 @@ describe("Staked Token", () => { const tx = await stakedToken["stake(uint256,bool)"](secondStakeAmount, true) const secondStakedTimestamp = await getTimestamp() - await expect(tx) - .to.emit(stakedToken, "Staked") - .withArgs(sa.default.address, secondStakeAmount, ZERO_ADDRESS) + await expect(tx).to.emit(stakedToken, "Staked").withArgs(sa.default.address, secondStakeAmount, ZERO_ADDRESS) await expect(tx).to.emit(stakedToken, "DelegateChanged").not await expect(tx) .to.emit(stakedToken, "DelegateVotesChanged") .withArgs(sa.default.address, stakedAmount.div(5), stakedAmount.add(secondStakeAmount)) - await expect(tx) - .to.emit(rewardToken, "Transfer") - .withArgs(sa.default.address, stakedToken.address, secondStakeAmount) - await expect(tx) - .to.emit(stakedToken, "CooldownExited") - .withArgs(sa.default.address) + await expect(tx).to.emit(rewardToken, "Transfer").withArgs(sa.default.address, stakedToken.address, secondStakeAmount) + await expect(tx).to.emit(stakedToken, "CooldownExited").withArgs(sa.default.address) const stakerDataAfter2ndStake = await snapshotUserStakingData(sa.default.address) expect(stakerDataAfter2ndStake.cooldownTimestamp, "cooldown timestamp after 2nd stake").to.eq(0) @@ -1361,9 +1294,7 @@ describe("Staked Token", () => { const withdrawAmount = simpleToExactAmount(100) const redemptionFee = withdrawAmount.div(10) const tx2 = await stakedToken.withdraw(withdrawAmount, sa.default.address, false, false) - await expect(tx2) - .to.emit(stakedToken, "Withdraw") - .withArgs(sa.default.address, sa.default.address, withdrawAmount) + await expect(tx2).to.emit(stakedToken, "Withdraw").withArgs(sa.default.address, sa.default.address, withdrawAmount) const afterData = await snapshotUserStakingData(sa.default.address) expect(afterData.stakedBalance, "staker staked after").to.eq(0) @@ -1381,9 +1312,7 @@ describe("Staked Token", () => { // fee = withdraw with fee / 1.1 * 0.1 = withdraw with fee / 11 const redemptionFee = stakedAmount.div(11) const tx2 = await stakedToken.withdraw(stakedAmount, sa.default.address, true, true) - await expect(tx2) - .to.emit(stakedToken, "Withdraw") - .withArgs(sa.default.address, sa.default.address, stakedAmount) + await expect(tx2).to.emit(stakedToken, "Withdraw").withArgs(sa.default.address, sa.default.address, stakedAmount) const afterData = await snapshotUserStakingData(sa.default.address) expect(afterData.stakedBalance, "staker stkRWD after").to.eq(0) @@ -1431,9 +1360,7 @@ describe("Staked Token", () => { const withdrawAmount = simpleToExactAmount(300) const redemptionFee = withdrawAmount.div(10) const tx2 = await stakedToken.withdraw(withdrawAmount, sa.default.address, false, false) - await expect(tx2) - .to.emit(stakedToken, "Withdraw") - .withArgs(sa.default.address, sa.default.address, withdrawAmount) + await expect(tx2).to.emit(stakedToken, "Withdraw").withArgs(sa.default.address, sa.default.address, withdrawAmount) const afterData = await snapshotUserStakingData(sa.default.address) console.log("data before") @@ -1462,9 +1389,7 @@ describe("Staked Token", () => { // fee = withdraw with fee / 1.1 * 0.1 = withdraw with fee / 11 const redemptionFee = cooldownAmount.div(11) const tx2 = await stakedToken.withdraw(cooldownAmount, sa.default.address, true, true) - await expect(tx2) - .to.emit(stakedToken, "Withdraw") - .withArgs(sa.default.address, sa.default.address, cooldownAmount) + await expect(tx2).to.emit(stakedToken, "Withdraw").withArgs(sa.default.address, sa.default.address, cooldownAmount) const afterData = await snapshotUserStakingData(sa.default.address) expect(afterData.stakedBalance, "staker stkRWD after").to.eq(remainingBalance) From b0a87b88d0051a76f56f27489162b3cb588feb2b Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 18 Aug 2021 22:59:23 +1000 Subject: [PATCH 03/10] feat: added questMaster setter --- .../governance/staking/GamifiedToken.sol | 35 ++++++++++++------- .../staking/GamifiedVotingToken.sol | 3 +- contracts/governance/staking/StakedToken.sol | 10 +++--- .../governance/staking/StakedTokenBPT.sol | 4 +-- tasks/deployBoostedVault.ts | 3 +- test/governance/staked-token.spec.ts | 24 +++++++++++-- 6 files changed, 52 insertions(+), 27 deletions(-) diff --git a/contracts/governance/staking/GamifiedToken.sol b/contracts/governance/staking/GamifiedToken.sol index e4f304cc..e24bb946 100644 --- a/contracts/governance/staking/GamifiedToken.sol +++ b/contracts/governance/staking/GamifiedToken.sol @@ -30,8 +30,6 @@ abstract contract GamifiedToken is ContextUpgradeable, HeadlessStakingRewards { - /// @notice address that signs user quests have been completed - address public immutable _signer; /// @notice name of this token (ERC20) string public override name; /// @notice symbol of this token (ERC20) @@ -48,8 +46,8 @@ abstract contract GamifiedToken is /// @notice Timestamp at which the current season started uint32 public seasonEpoch; - /// @notice A whitelisted questMaster who can add quests - address internal _questMaster; + /// @notice A whitelisted questMaster who can administer quests including signing user quests are completed. + address public questMaster; event QuestAdded( address questMaster, @@ -62,38 +60,38 @@ abstract contract GamifiedToken is event QuestComplete(address indexed user, uint256 indexed id); event QuestExpired(uint16 indexed id); event QuestSeasonEnded(); + event QuestMaster(address oldQuestMaster, address newQuestMaster); /*************************************** INIT ****************************************/ /** - * @param _signerArg Signer address is used to verify completion of quests off chain * @param _nexus System nexus * @param _rewardsToken Token that is being distributed as a reward. eg MTA */ constructor( - address _signerArg, address _nexus, address _rewardsToken - ) HeadlessStakingRewards(_nexus, _rewardsToken) { - _signer = _signerArg; - } + ) HeadlessStakingRewards(_nexus, _rewardsToken) {} /** * @param _nameArg Token name * @param _symbolArg Token symbol * @param _rewardsDistributorArg mStable Rewards Distributor + * @param _questMaster account that can sign user quests as completed */ function __GamifiedToken_init( string memory _nameArg, string memory _symbolArg, - address _rewardsDistributorArg + address _rewardsDistributorArg, + address _questMaster ) internal initializer { __Context_init_unchained(); name = _nameArg; symbol = _symbolArg; seasonEpoch = SafeCast.toUint32(block.timestamp); + questMaster = _questMaster; HeadlessStakingRewards._initialize(_rewardsDistributorArg); } @@ -106,7 +104,20 @@ abstract contract GamifiedToken is } function _questMasterOrGovernor() internal view { - require(_msgSender() == _questMaster || _msgSender() == _governor(), "Not verified"); + require(_msgSender() == questMaster || _msgSender() == _governor(), "Not verified"); + } + + /*************************************** + Admin + ****************************************/ + + /** + * @dev Sets the quest master that can sign user quests as being completed + */ + function setQuestMaster(address _newQuestMaster) external questMasterOrGovernor() { + emit QuestMaster(questMaster, _newQuestMaster); + + questMaster = _newQuestMaster; } /*************************************** @@ -239,7 +250,7 @@ abstract contract GamifiedToken is require(_validQuest(_ids[i]), "Err: Invalid Quest"); require(!hasCompleted(_account, _ids[i]), "Err: Already Completed"); require( - SignatureVerifier.verify(_signer, _account, _ids[i], _signatures[i]), + SignatureVerifier.verify(questMaster, _account, _ids[i], _signatures[i]), "Err: Invalid Signature" ); diff --git a/contracts/governance/staking/GamifiedVotingToken.sol b/contracts/governance/staking/GamifiedVotingToken.sol index 8c288fba..ac68e3fe 100644 --- a/contracts/governance/staking/GamifiedVotingToken.sol +++ b/contracts/governance/staking/GamifiedVotingToken.sol @@ -55,10 +55,9 @@ abstract contract GamifiedVotingToken is Initializable, GamifiedToken { ); constructor( - address _signer, address _nexus, address _rewardsToken - ) GamifiedToken(_signer, _nexus, _rewardsToken) {} + ) GamifiedToken(_nexus, _rewardsToken) {} function __GamifiedVotingToken_init() internal initializer {} diff --git a/contracts/governance/staking/StakedToken.sol b/contracts/governance/staking/StakedToken.sol index 37698538..23309ea7 100644 --- a/contracts/governance/staking/StakedToken.sol +++ b/contracts/governance/staking/StakedToken.sol @@ -77,7 +77,6 @@ contract StakedToken is IStakedToken, GamifiedVotingToken { ****************************************/ /** - * @param _signer Signer address is used to verify completion of quests off chain * @param _nexus System nexus * @param _rewardsToken Token that is being distributed as a reward. eg MTA * @param _stakedToken Core token that is staked and tracked (e.g. MTA) @@ -85,13 +84,12 @@ contract StakedToken is IStakedToken, GamifiedVotingToken { * @param _unstakeWindow Window in which it is possible to withdraw, following the cooldown period */ constructor( - address _signer, address _nexus, address _rewardsToken, address _stakedToken, uint256 _cooldownSeconds, uint256 _unstakeWindow - ) GamifiedVotingToken(_signer, _nexus, _rewardsToken) { + ) GamifiedVotingToken(_nexus, _rewardsToken) { STAKED_TOKEN = IERC20(_stakedToken); COOLDOWN_SECONDS = _cooldownSeconds; UNSTAKE_WINDOW = _unstakeWindow; @@ -101,13 +99,15 @@ contract StakedToken is IStakedToken, GamifiedVotingToken { * @param _nameArg Token name * @param _symbolArg Token symbol * @param _rewardsDistributorArg mStable Rewards Distributor + * @param _questMaster account that signs user quests as completed */ function initialize( string memory _nameArg, string memory _symbolArg, - address _rewardsDistributorArg + address _rewardsDistributorArg, + address _questMaster ) external initializer { - __GamifiedToken_init(_nameArg, _symbolArg, _rewardsDistributorArg); + __GamifiedToken_init(_nameArg, _symbolArg, _rewardsDistributorArg, _questMaster); safetyData = SafetyData({ collateralisationRatio: 1e18, slashingPercentage: 0 }); } diff --git a/contracts/governance/staking/StakedTokenBPT.sol b/contracts/governance/staking/StakedTokenBPT.sol index f5490e82..66710159 100644 --- a/contracts/governance/staking/StakedTokenBPT.sol +++ b/contracts/governance/staking/StakedTokenBPT.sol @@ -24,7 +24,6 @@ contract StakedTokenBPT is StakedToken { event BalRecipientChanged(address newRecipient); /** - * @param _signer Signer address is used to verify completion of quests off chain * @param _nexus System nexus * @param _rewardsToken Token that is being distributed as a reward. eg MTA * @param _stakedToken Core token that is staked and tracked (e.g. MTA) @@ -33,14 +32,13 @@ contract StakedTokenBPT is StakedToken { * @param _bal Balancer addresses, [0] = $BAL addr, [1] = designated recipient */ constructor( - address _signer, address _nexus, address _rewardsToken, address _stakedToken, uint256 _cooldownSeconds, uint256 _unstakeWindow, address[2] memory _bal - ) StakedToken(_signer, _nexus, _rewardsToken, _stakedToken, _cooldownSeconds, _unstakeWindow) { + ) StakedToken(_nexus, _rewardsToken, _stakedToken, _cooldownSeconds, _unstakeWindow) { BAL = IERC20(_bal[0]); balRecipient = _bal[1]; } diff --git a/tasks/deployBoostedVault.ts b/tasks/deployBoostedVault.ts index c80e5c69..514698b4 100644 --- a/tasks/deployBoostedVault.ts +++ b/tasks/deployBoostedVault.ts @@ -127,7 +127,6 @@ task("StakedToken.deploy", "Deploys a Staked Token behind a proxy") const stakedTokenFactory = new StakedToken__factory(stakedTokenLibraryAddresses, deployer.signer) console.log(`Staked contract size ${StakedToken__factory.bytecode.length / 2} bytes`) const stakedTokenImpl = await deployContract(stakedTokenFactory, "StakedToken", [ - taskArgs.questSignerAddress, nexusAddress, rewardsTokenAddress, rewardsTokenAddress, @@ -135,7 +134,7 @@ task("StakedToken.deploy", "Deploys a Staked Token behind a proxy") ONE_DAY.mul(2), ]) - const data = stakedTokenImpl.interface.encodeFunctionData("initialize", [taskArgs.name, taskArgs.symbol, rewardsDistributorAddress]) + const data = stakedTokenImpl.interface.encodeFunctionData("initialize", [taskArgs.name, taskArgs.symbol, rewardsDistributorAddress, taskArgs.questSignerAddress]) await deployContract(new AssetProxy__factory(deployer.signer), "AssetProxy", [stakedTokenImpl.address, deployer.address, data]) }) diff --git a/test/governance/staked-token.spec.ts b/test/governance/staked-token.spec.ts index ddb08e0c..c3be3e4d 100644 --- a/test/governance/staked-token.spec.ts +++ b/test/governance/staked-token.spec.ts @@ -58,7 +58,6 @@ describe("Staked Token", () => { } const stakedTokenFactory = new StakedToken__factory(stakedTokenLibraryAddresses, sa.default.signer) const stakedTokenImpl = await stakedTokenFactory.deploy( - sa.questSigner.address, nexus.address, rewardToken.address, rewardToken.address, @@ -66,7 +65,7 @@ describe("Staked Token", () => { ONE_DAY.mul(2), ) const rewardsDistributorAddress = DEAD_ADDRESS - const data = stakedTokenImpl.interface.encodeFunctionData("initialize", ["Staked Rewards", "stkRWD", rewardsDistributorAddress]) + const data = stakedTokenImpl.interface.encodeFunctionData("initialize", ["Staked Rewards", "stkRWD", rewardsDistributorAddress, sa.questSigner.address]) const stakedTokenProxy = await new AssetProxy__factory(sa.default.signer).deploy(stakedTokenImpl.address, DEAD_ADDRESS, data) return stakedTokenFactory.attach(stakedTokenProxy.address) @@ -113,7 +112,7 @@ describe("Staked Token", () => { expect(await stakedToken.COOLDOWN_PERCENTAGE_SCALE(), "unstake window").to.eq(cooldown100Percentage) // eslint-disable-next-line no-underscore-dangle - expect(await stakedToken._signer(), "quest signer").to.eq(sa.questSigner.address) + expect(await stakedToken.questMaster(), "quest master").to.eq(sa.questSigner.address) }) }) context("staking and delegating", () => { @@ -869,6 +868,25 @@ describe("Staked Token", () => { }) }) }) + context("questMaster", () => { + beforeEach(async () => { + stakedToken = await redeployStakedToken() + expect(await stakedToken.questMaster(), "quest master before").to.eq(sa.questSigner.address) + }) + it("should set questMaster by governor", async () => { + const tx = await stakedToken.connect(sa.governor.signer).setQuestMaster(sa.dummy1.address); + await expect(tx).to.emit(stakedToken, "QuestMaster").withArgs(sa.questSigner.address, sa.dummy1.address); + expect(await stakedToken.questMaster(), "quest master after").to.eq(sa.dummy1.address) + }) + it("should set questMaster by quest master", async () => { + const tx = await stakedToken.connect(sa.questSigner.signer).setQuestMaster(sa.dummy2.address); + await expect(tx).to.emit(stakedToken, "QuestMaster").withArgs(sa.questSigner.address, sa.dummy2.address); + expect(await stakedToken.questMaster(), "quest master after").to.eq(sa.dummy2.address) + }) + it("should fail to set quest master by anyone", async () => { + await expect(stakedToken.connect(sa.dummy3.signer).setQuestMaster(sa.dummy3.address)).to.revertedWith("Not verified") + }) + }) // Important that each action (checkTimestamp, completeQuest, mint) applies this because // scaledBalance could actually decrease, even in these situations, since old seasonMultipliers are slashed From bbee90e1fa7caca1c04694ac82b922076fb87fa4 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 19 Aug 2021 15:16:18 +1000 Subject: [PATCH 04/10] chore: added staked token whitelist tests --- contracts/governance/staking/StakedToken.sol | 2 +- .../z_mocks/governance/stakedTokenWrapper.sol | 30 ++++++++++ test/governance/staked-token.spec.ts | 56 ++++++++++++++++--- 3 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 contracts/z_mocks/governance/stakedTokenWrapper.sol diff --git a/contracts/governance/staking/StakedToken.sol b/contracts/governance/staking/StakedToken.sol index 23309ea7..e04fcca3 100644 --- a/contracts/governance/staking/StakedToken.sol +++ b/contracts/governance/staking/StakedToken.sol @@ -144,7 +144,7 @@ contract StakedToken is IStakedToken, GamifiedVotingToken { if (_msgSender() != tx.origin) { require( whitelistedWrappers[_msgSender()], - "Transactions from non-whitelisted smart contracts not allowed" + "Not a whitelisted contract" ); } } diff --git a/contracts/z_mocks/governance/stakedTokenWrapper.sol b/contracts/z_mocks/governance/stakedTokenWrapper.sol new file mode 100644 index 00000000..87649086 --- /dev/null +++ b/contracts/z_mocks/governance/stakedTokenWrapper.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.6; + +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { StakedToken } from "../../governance/staking/StakedToken.sol"; + +/** + * Used to test contract interactions with the StakedToken + */ +contract StakedTokenWrapper { + using SafeERC20 for IERC20; + + IERC20 public rewardsToken; + StakedToken public stakedToken; + + constructor(address _rewardsToken, address _stakedToken) { + stakedToken = StakedToken(_stakedToken); + rewardsToken = IERC20(_rewardsToken); + rewardsToken.safeApprove(_stakedToken, 2**256 - 1); + } + + function stake(uint256 _amount) external { + stakedToken.stake(_amount); + } + + function stake(uint256 _amount, address _delegatee) external { + stakedToken.stake(_amount, _delegatee); + } +} diff --git a/test/governance/staked-token.spec.ts b/test/governance/staked-token.spec.ts index c3be3e4d..9273e52e 100644 --- a/test/governance/staked-token.spec.ts +++ b/test/governance/staked-token.spec.ts @@ -13,6 +13,7 @@ import { PlatformTokenVendorFactory__factory, SignatureVerifier__factory, StakedToken, + StakedTokenWrapper__factory, StakedToken__factory, } from "types" import { assertBNClose, DEAD_ADDRESS } from "index" @@ -65,7 +66,12 @@ describe("Staked Token", () => { ONE_DAY.mul(2), ) const rewardsDistributorAddress = DEAD_ADDRESS - const data = stakedTokenImpl.interface.encodeFunctionData("initialize", ["Staked Rewards", "stkRWD", rewardsDistributorAddress, sa.questSigner.address]) + const data = stakedTokenImpl.interface.encodeFunctionData("initialize", [ + "Staked Rewards", + "stkRWD", + rewardsDistributorAddress, + sa.questSigner.address, + ]) const stakedTokenProxy = await new AssetProxy__factory(sa.default.signer).deploy(stakedTokenImpl.address, DEAD_ADDRESS, data) return stakedTokenFactory.attach(stakedTokenProxy.address) @@ -874,13 +880,13 @@ describe("Staked Token", () => { expect(await stakedToken.questMaster(), "quest master before").to.eq(sa.questSigner.address) }) it("should set questMaster by governor", async () => { - const tx = await stakedToken.connect(sa.governor.signer).setQuestMaster(sa.dummy1.address); - await expect(tx).to.emit(stakedToken, "QuestMaster").withArgs(sa.questSigner.address, sa.dummy1.address); + const tx = await stakedToken.connect(sa.governor.signer).setQuestMaster(sa.dummy1.address) + await expect(tx).to.emit(stakedToken, "QuestMaster").withArgs(sa.questSigner.address, sa.dummy1.address) expect(await stakedToken.questMaster(), "quest master after").to.eq(sa.dummy1.address) }) it("should set questMaster by quest master", async () => { - const tx = await stakedToken.connect(sa.questSigner.signer).setQuestMaster(sa.dummy2.address); - await expect(tx).to.emit(stakedToken, "QuestMaster").withArgs(sa.questSigner.address, sa.dummy2.address); + const tx = await stakedToken.connect(sa.questSigner.signer).setQuestMaster(sa.dummy2.address) + await expect(tx).to.emit(stakedToken, "QuestMaster").withArgs(sa.questSigner.address, sa.dummy2.address) expect(await stakedToken.questMaster(), "quest master after").to.eq(sa.dummy2.address) }) it("should fail to set quest master by anyone", async () => { @@ -1428,8 +1434,44 @@ describe("Staked Token", () => { }) context("interacting from a smart contract", () => { - // Will need to create a sample solidity mock wrapper that has the ability to deposit and withdraw - it("should not be possible to stake and withdraw from a smart contract") + let stakedTokenWrapper + const stakedAmount = simpleToExactAmount(1000) + before(async () => { + stakedToken = await redeployStakedToken() + + stakedTokenWrapper = await new StakedTokenWrapper__factory(sa.default.signer).deploy(rewardToken.address, stakedToken.address) + await rewardToken.transfer(stakedTokenWrapper.address, stakedAmount.mul(2)) + }) + it("should not be possible to stake when not whitelisted", async () => { + await expect(stakedTokenWrapper["stake(uint256)"](stakedAmount)).to.revertedWith("Not a whitelisted contract") + }) + it("should allow governor to whitelist a contract", async () => { + expect(await stakedToken.whitelistedWrappers(stakedTokenWrapper.address), "wrapper not whitelisted before").to.be.false + const tx = await stakedToken.connect(sa.governor.signer).whitelistWrapper(stakedTokenWrapper.address) + await expect(tx).to.emit(stakedToken, "WrapperWhitelisted").withArgs(stakedTokenWrapper.address) + expect(await stakedToken.whitelistedWrappers(stakedTokenWrapper.address), "wrapper whitelisted after").to.be.true + + const tx2 = await stakedTokenWrapper["stake(uint256)"](stakedAmount) + await expect(tx2).to.emit(stakedToken, "Staked").withArgs(stakedTokenWrapper.address, stakedAmount, ZERO_ADDRESS) + }) + it("should allow governor to blacklist a contract", async () => { + const tx = await stakedToken.connect(sa.governor.signer).whitelistWrapper(stakedTokenWrapper.address) + await expect(tx).to.emit(stakedToken, "WrapperWhitelisted").withArgs(stakedTokenWrapper.address) + expect(await stakedToken.whitelistedWrappers(stakedTokenWrapper.address), "wrapper whitelisted").to.be.true + + const tx2 = await stakedToken.connect(sa.governor.signer).blackListWrapper(stakedTokenWrapper.address) + await expect(tx2).to.emit(stakedToken, "WrapperBlacklisted").withArgs(stakedTokenWrapper.address) + expect(await stakedToken.whitelistedWrappers(stakedTokenWrapper.address), "wrapper not whitelisted").to.be.false + + await expect(stakedTokenWrapper["stake(uint256)"](stakedAmount)).to.revertedWith("Not a whitelisted contract") + }) + it("Votes can be delegated to a smart contract", async () => { + await rewardToken.connect(sa.default.signer).approve(stakedToken.address, stakedAmount) + + const tx = await stakedToken["stake(uint256,address)"](stakedAmount, stakedTokenWrapper.address) + + await expect(tx).to.emit(stakedToken, "Staked").withArgs(stakedTokenWrapper.address, stakedAmount, stakedTokenWrapper.address) + }) }) context("updating lastAction timestamp", () => { From 6e87f6a16705428d9f0d0677572b856a572b826f Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 19 Aug 2021 15:39:25 +1000 Subject: [PATCH 05/10] chore: more whitelist tests --- .../z_mocks/governance/stakedTokenWrapper.sol | 9 ++++++ package.json | 2 +- test/governance/staked-token.spec.ts | 31 +++++++++++++++---- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/contracts/z_mocks/governance/stakedTokenWrapper.sol b/contracts/z_mocks/governance/stakedTokenWrapper.sol index 87649086..c1da402b 100644 --- a/contracts/z_mocks/governance/stakedTokenWrapper.sol +++ b/contracts/z_mocks/governance/stakedTokenWrapper.sol @@ -27,4 +27,13 @@ contract StakedTokenWrapper { function stake(uint256 _amount, address _delegatee) external { stakedToken.stake(_amount, _delegatee); } + + function withdraw( + uint256 _amount, + address _recipient, + bool _amountIncludesFee, + bool _exitCooldown + ) external { + stakedToken.withdraw(_amount, _recipient, _amountIncludesFee, _exitCooldown); + } } diff --git a/package.json b/package.json index 2bd3edbc..2dd1f4f3 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "scripts": { "prettify": "", "lint": "yarn run lint-ts; yarn run lint-sol", - "lint:fix": "yarn pretty-quick --pattern '**/*.*(sol|json)' --staged --verbose && yarn prettier --write tasks/**/*.ts test/**/*.ts types/*.ts", + "lint:fix": "yarn pretty-quick --pattern '**/*.*(sol|json)' --staged --verbose && yarn pretty-quick --staged --write tasks/**/*.ts test/**/*.ts types/*.ts", "lint-ts": "yarn eslint ./test --ext .ts --fix --quiet", "lint-sol": "solhint 'contracts/**/*.sol'", "compile-abis": "typechain --target=ethers-v5 --out-dir types/generated \"?(contracts|build)/!(build-info)/**/+([a-zA-Z0-9]).json\"", diff --git a/test/governance/staked-token.spec.ts b/test/governance/staked-token.spec.ts index 9273e52e..186ca209 100644 --- a/test/governance/staked-token.spec.ts +++ b/test/governance/staked-token.spec.ts @@ -1432,7 +1432,6 @@ describe("Staked Token", () => { it("distribute these pendingAdditionalReward with the next notification") }) }) - context("interacting from a smart contract", () => { let stakedTokenWrapper const stakedAmount = simpleToExactAmount(1000) @@ -1440,10 +1439,7 @@ describe("Staked Token", () => { stakedToken = await redeployStakedToken() stakedTokenWrapper = await new StakedTokenWrapper__factory(sa.default.signer).deploy(rewardToken.address, stakedToken.address) - await rewardToken.transfer(stakedTokenWrapper.address, stakedAmount.mul(2)) - }) - it("should not be possible to stake when not whitelisted", async () => { - await expect(stakedTokenWrapper["stake(uint256)"](stakedAmount)).to.revertedWith("Not a whitelisted contract") + await rewardToken.transfer(stakedTokenWrapper.address, stakedAmount.mul(3)) }) it("should allow governor to whitelist a contract", async () => { expect(await stakedToken.whitelistedWrappers(stakedTokenWrapper.address), "wrapper not whitelisted before").to.be.false @@ -1470,7 +1466,30 @@ describe("Staked Token", () => { const tx = await stakedToken["stake(uint256,address)"](stakedAmount, stakedTokenWrapper.address) - await expect(tx).to.emit(stakedToken, "Staked").withArgs(stakedTokenWrapper.address, stakedAmount, stakedTokenWrapper.address) + await expect(tx).to.emit(stakedToken, "Staked").withArgs(sa.default.address, stakedAmount, stakedTokenWrapper.address) + }) + context("should not", () => { + it("be possible to stake when not whitelisted", async () => { + await expect(stakedTokenWrapper["stake(uint256)"](stakedAmount)).to.revertedWith("Not a whitelisted contract") + }) + it("be possible to withdraw when not whitelisted", async () => { + await stakedToken.connect(sa.governor.signer).whitelistWrapper(stakedTokenWrapper.address) + await stakedTokenWrapper["stake(uint256)"](stakedAmount) + await stakedToken.connect(sa.governor.signer).blackListWrapper(stakedTokenWrapper.address) + const tx = stakedTokenWrapper.withdraw(stakedAmount, sa.default.address, true, true) + + await expect(tx).to.revertedWith("Not a whitelisted contract") + }) + it("allow non governor to whitelist a contract", async () => { + const tx = stakedToken.whitelistWrapper(stakedTokenWrapper.address) + await expect(tx).to.revertedWith("Only governor can execute") + }) + it("allow non governor to blacklist a contract", async () => { + await stakedToken.connect(sa.governor.signer).whitelistWrapper(stakedTokenWrapper.address) + + const tx = stakedToken.blackListWrapper(stakedTokenWrapper.address) + await expect(tx).to.revertedWith("Only governor can execute") + }) }) }) From cc0b781c824a693034ce14088132c90b8541bd0c Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 19 Aug 2021 22:04:43 +1000 Subject: [PATCH 06/10] chore: added recollateralisation tests --- contracts/governance/staking/StakedToken.sol | 10 +- contracts/shared/ImmutableModule.sol | 4 +- contracts/z_mocks/nexus/MockNexus.sol | 4 + test-utils/machines/standardAccounts.ts | 3 + test/governance/staked-token.spec.ts | 96 +++++++++++++++++++- 5 files changed, 103 insertions(+), 14 deletions(-) diff --git a/contracts/governance/staking/StakedToken.sol b/contracts/governance/staking/StakedToken.sol index e04fcca3..c913ec0a 100644 --- a/contracts/governance/staking/StakedToken.sol +++ b/contracts/governance/staking/StakedToken.sol @@ -2,8 +2,6 @@ pragma solidity 0.8.6; pragma abicoder v2; -import "hardhat/console.sol"; - import { IStakedToken } from "./interfaces/IStakedToken.sol"; import { GamifiedVotingToken } from "./GamifiedVotingToken.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -142,10 +140,7 @@ contract StakedToken is IStakedToken, GamifiedVotingToken { function _assertNotContract() internal view { if (_msgSender() != tx.origin) { - require( - whitelistedWrappers[_msgSender()], - "Not a whitelisted contract" - ); + require(whitelistedWrappers[_msgSender()], "Not a whitelisted contract"); } } @@ -349,8 +344,6 @@ contract StakedToken is IStakedToken, GamifiedVotingToken { ((maxWithdrawal - totalWithdraw) * COOLDOWN_PERCENTAGE_SCALE) / (uint256(balance.raw) - totalWithdraw) ); - console.log("max, total, user"); - console.log(maxWithdrawal, totalWithdraw, userWithdrawal); stakersCooldowns[_msgSender()].percentage = cooldownPercentage; } @@ -445,7 +438,6 @@ contract StakedToken is IStakedToken, GamifiedVotingToken { onlyGovernor onlyBeforeRecollateralisation { - require(safetyData.collateralisationRatio == 1e18, "Process already begun"); require(_newRate <= 5e17, "Cannot exceed 50%"); safetyData.slashingPercentage = SafeCast.toUint128(_newRate); diff --git a/contracts/shared/ImmutableModule.sol b/contracts/shared/ImmutableModule.sol index f03b50bd..1b408aca 100644 --- a/contracts/shared/ImmutableModule.sol +++ b/contracts/shared/ImmutableModule.sol @@ -79,8 +79,8 @@ abstract contract ImmutableModule is ModuleKeys { } /** - * @dev Return Recollateraliser Module address from the Nexus - * @return Address of the Recollateraliser Module contract (Phase 2) + * @dev Return Liquidator Module address from the Nexus + * @return Address of the Liquidator Module contract */ function _liquidator() internal view returns (address) { return nexus.getModule(KEY_LIQUIDATOR); diff --git a/contracts/z_mocks/nexus/MockNexus.sol b/contracts/z_mocks/nexus/MockNexus.sol index 533b90b8..54182e9f 100644 --- a/contracts/z_mocks/nexus/MockNexus.sol +++ b/contracts/z_mocks/nexus/MockNexus.sol @@ -39,4 +39,8 @@ contract MockNexus is ModuleKeys { function setLiquidator(address _liquidator) external { modules[KEY_LIQUIDATOR] = _liquidator; } + + function setRecollateraliser(address _recollateraliser) external { + modules[KEY_RECOLLATERALISER] = _recollateraliser; + } } diff --git a/test-utils/machines/standardAccounts.ts b/test-utils/machines/standardAccounts.ts index 5a944935..9445fd04 100644 --- a/test-utils/machines/standardAccounts.ts +++ b/test-utils/machines/standardAccounts.ts @@ -34,6 +34,8 @@ export class StandardAccounts { public mockInterestValidator: Account + public mockRecollateraliser: Account + public mockMasset: Account public async initAccounts(signers: Signer[]): Promise { @@ -56,6 +58,7 @@ export class StandardAccounts { this.questSigner, this.mockSavingsManager, this.mockInterestValidator, + this.mockRecollateraliser, this.mockMasset, ] = this.all return this diff --git a/test/governance/staked-token.spec.ts b/test/governance/staked-token.spec.ts index 186ca209..17333526 100644 --- a/test/governance/staked-token.spec.ts +++ b/test/governance/staked-token.spec.ts @@ -119,6 +119,10 @@ describe("Staked Token", () => { // eslint-disable-next-line no-underscore-dangle expect(await stakedToken.questMaster(), "quest master").to.eq(sa.questSigner.address) + + const safetyData = await stakedToken.safetyData() + expect(safetyData.collateralisationRatio, "Collateralisation ratio").to.eq(simpleToExactAmount(1)) + expect(safetyData.slashingPercentage, "Slashing percentage").to.eq(0) }) }) context("staking and delegating", () => { @@ -1254,7 +1258,7 @@ describe("Staked Token", () => { }) }) }) - context.skip("withdraw", () => { + context("withdraw", () => { const stakedAmount = simpleToExactAmount(2000) let cooldownTimestamp: BN context("should not be possible", () => { @@ -1390,8 +1394,8 @@ describe("Staked Token", () => { console.log("data before") console.log(beforeData.cooldownPercentage.toString(), beforeData.userBalances.raw.toString()) console.log(afterData.cooldownPercentage.toString(), afterData.userBalances.raw.toString()) - expect(afterData.stakedBalance, "staker staked after").to.eq(remainingBalance) - expect(afterData.votes, "staker votes after").to.eq(remainingBalance) + // expect(afterData.stakedBalance, "staker staked after").to.eq(remainingBalance) + // expect(afterData.votes, "staker votes after").to.eq(remainingBalance) expect(afterData.cooldownTimestamp, "cooldown timestamp after").to.eq(beforeData.cooldownTimestamp) // 1400 / 2000 * 100 = 70 // (1400 - 300 - 30) / (2000 - 300 - 30) * 1e18 = 64.0718563e16 @@ -1492,7 +1496,93 @@ describe("Staked Token", () => { }) }) }) + context("recollateralisation", () => { + const stakedAmount = simpleToExactAmount(10000) + beforeEach(async () => { + stakedToken = await redeployStakedToken() + await nexus.setRecollateraliser(sa.mockRecollateraliser.address) + const users = [sa.default, sa.dummy1, sa.dummy2, sa.dummy3, sa.dummy4] + for (const user of users) { + await rewardToken.transfer(user.address, stakedAmount) + await rewardToken.connect(user.signer).approve(stakedToken.address, stakedAmount) + await stakedToken.connect(user.signer)["stake(uint256,address)"](stakedAmount, user.address) + } + }) + it("should allow governor to set 25% slashing", async () => { + const slashingPercentage = simpleToExactAmount(25, 16) + const tx = await stakedToken.connect(sa.governor.signer).changeSlashingPercentage(slashingPercentage) + await expect(tx).to.emit(stakedToken, "SlashRateChanged").withArgs(slashingPercentage) + + const safetyDataAfter = await stakedToken.safetyData() + expect(await safetyDataAfter.slashingPercentage, "slashing percentage after").to.eq(slashingPercentage) + expect(await safetyDataAfter.collateralisationRatio, "collateralisation ratio after").to.eq(simpleToExactAmount(1)) + }) + it("should allow governor to slash a second time before recollateralisation", async () => { + const firstSlashingPercentage = simpleToExactAmount(10, 16) + const secondSlashingPercentage = simpleToExactAmount(20, 16) + await stakedToken.connect(sa.governor.signer).changeSlashingPercentage(firstSlashingPercentage) + const tx = stakedToken.connect(sa.governor.signer).changeSlashingPercentage(secondSlashingPercentage) + await expect(tx).to.emit(stakedToken, "SlashRateChanged").withArgs(secondSlashingPercentage) + + const safetyDataAfter = await stakedToken.safetyData() + expect(await safetyDataAfter.slashingPercentage, "slashing percentage after").to.eq(secondSlashingPercentage) + expect(await safetyDataAfter.collateralisationRatio, "collateralisation ratio after").to.eq(simpleToExactAmount(1)) + }) + it("should allow recollateralisation", async () => { + const slashingPercentage = simpleToExactAmount(25, 16) + await stakedToken.connect(sa.governor.signer).changeSlashingPercentage(slashingPercentage) + + const tx = stakedToken.connect(sa.mockRecollateraliser.signer).emergencyRecollateralisation() + + // Events + await expect(tx).to.emit(stakedToken, "Recollateralised") + // transfer amount = 5 * 10,000 * 25% = 12,500 + await expect(tx) + .to.emit(rewardToken, "Transfer") + .withArgs(stakedToken.address, sa.mockRecollateraliser.address, simpleToExactAmount(12500)) + + const safetyDataAfter = await stakedToken.safetyData() + expect(await safetyDataAfter.slashingPercentage, "slashing percentage after").to.eq(slashingPercentage) + expect(await safetyDataAfter.collateralisationRatio, "collateralisation ratio after").to.eq( + simpleToExactAmount(1).sub(slashingPercentage), + ) + }) + context("should not allow", () => { + const slashingPercentage = simpleToExactAmount(10, 16) + it("governor to slash after recollateralisation", async () => { + await stakedToken.connect(sa.governor.signer).changeSlashingPercentage(slashingPercentage) + await stakedToken.connect(sa.mockRecollateraliser.signer).emergencyRecollateralisation() + + const tx = stakedToken.connect(sa.governor.signer).changeSlashingPercentage(slashingPercentage) + await expect(tx).to.revertedWith("Only while fully collateralised") + }) + it("slash percentage > 50%", async () => { + const tx = stakedToken.connect(sa.governor.signer).changeSlashingPercentage(simpleToExactAmount(51, 16)) + await expect(tx).to.revertedWith("Cannot exceed 50%") + }) + it("non governor to change slash percentage", async () => { + const tx = stakedToken.changeSlashingPercentage(slashingPercentage) + await expect(tx).to.revertedWith("Only governor can execute") + }) + it("non recollateralisation module to recollateralisation", async () => { + await stakedToken.connect(sa.governor.signer).changeSlashingPercentage(slashingPercentage) + const tx = stakedToken.connect(sa.default.signer).emergencyRecollateralisation() + await expect(tx).to.revertedWith("Only Recollateralisation Module") + }) + it("governor to recollateralisation", async () => { + await stakedToken.connect(sa.governor.signer).changeSlashingPercentage(slashingPercentage) + const tx = stakedToken.connect(sa.governor.signer).emergencyRecollateralisation() + await expect(tx).to.revertedWith("Only Recollateralisation Module") + }) + it("a second recollateralisation", async () => { + await stakedToken.connect(sa.governor.signer).changeSlashingPercentage(slashingPercentage) + await stakedToken.connect(sa.mockRecollateraliser.signer).emergencyRecollateralisation() + const tx = stakedToken.connect(sa.mockRecollateraliser.signer).emergencyRecollateralisation() + await expect(tx).to.revertedWith("Only while fully collateralised") + }) + }) + }) context("updating lastAction timestamp", () => { it("should be triggered after every WRITE action on the contract") }) From 010dae84f998ffce65279db7dab2c828bb0758f0 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 20 Aug 2021 13:53:29 +1000 Subject: [PATCH 07/10] chore: added staked token backward compatibility test --- test/governance/staked-token.spec.ts | 140 ++++++++++++++++++++++++++- 1 file changed, 139 insertions(+), 1 deletion(-) diff --git a/test/governance/staked-token.spec.ts b/test/governance/staked-token.spec.ts index 17333526..c622d6b4 100644 --- a/test/governance/staked-token.spec.ts +++ b/test/governance/staked-token.spec.ts @@ -47,6 +47,7 @@ describe("Staked Token", () => { const redeployStakedToken = async (): Promise => { deployTime = await getTimestamp() nexus = await new MockNexus__factory(sa.default.signer).deploy(sa.governor.address, DEAD_ADDRESS, DEAD_ADDRESS) + await nexus.setRecollateraliser(sa.mockRecollateraliser.address) rewardToken = await new MockERC20__factory(sa.default.signer).deploy("Reward", "RWD", 18, sa.default.address, 10000000) const signatureVerifier = await new SignatureVerifier__factory(sa.default.signer).deploy() @@ -1435,6 +1436,144 @@ describe("Staked Token", () => { it("apply a redemption fee which is added to the pendingRewards from the rewards contract") it("distribute these pendingAdditionalReward with the next notification") }) + context("after 25% slashing and recollateralisation", () => { + const slashingPercentage = simpleToExactAmount(25, 16) + beforeEach(async () => { + stakedToken = await redeployStakedToken() + await rewardToken.connect(sa.default.signer).approve(stakedToken.address, stakedAmount) + await stakedToken["stake(uint256,address)"](stakedAmount, sa.default.address) + await increaseTime(ONE_DAY.mul(7).add(60)) + await stakedToken.connect(sa.governor.signer).changeSlashingPercentage(slashingPercentage) + await stakedToken.connect(sa.mockRecollateraliser.signer).emergencyRecollateralisation() + }) + it("should withdraw all incl fee and get 75% of rewards", async () => { + const tx = await stakedToken.withdraw(stakedAmount, sa.default.address, true, false) + await expect(tx).to.emit(stakedToken, "Withdraw").withArgs(sa.default.address, sa.default.address, stakedAmount) + await expect(tx).to.emit(rewardToken, "Transfer").withArgs(stakedToken.address, sa.default.address, stakedAmount.mul(3).div(4)) + + const afterData = await snapshotUserStakingData(sa.default.address) + expect(afterData.stakedBalance, "staker stkRWD after").to.eq(0) + expect(afterData.votes, "staker votes after").to.eq(0) + expect(afterData.cooldownTimestamp, "staked cooldown start").to.eq(0) + expect(afterData.cooldownPercentage, "staked cooldown percentage").to.eq(0) + expect(afterData.userBalances.cooldownMultiplier, "cooldown multiplier after").to.eq(0) + expect(afterData.userBalances.raw, "staked raw balance after").to.eq(0) + }) + it("should withdraw all excl. fee and get 75% of rewards", async () => { + const tx = await stakedToken.withdraw(stakedAmount, sa.default.address, false, false) + await expect(tx).to.emit(stakedToken, "Withdraw").withArgs(sa.default.address, sa.default.address, stakedAmount) + await expect(tx).to.emit(rewardToken, "Transfer").withArgs(stakedToken.address, sa.default.address, stakedAmount.mul(3).div(4)) + + const afterData = await snapshotUserStakingData(sa.default.address) + expect(afterData.stakedBalance, "staker stkRWD after").to.eq(0) + expect(afterData.votes, "staker votes after").to.eq(0) + expect(afterData.cooldownTimestamp, "staked cooldown start").to.eq(0) + expect(afterData.cooldownPercentage, "staked cooldown percentage").to.eq(0) + expect(afterData.userBalances.cooldownMultiplier, "cooldown multiplier after").to.eq(0) + expect(afterData.userBalances.raw, "staked raw balance after").to.eq(0) + }) + it("should partial withdraw and get 75% of rewards", async () => { + const withdrawAmount = stakedAmount.div(10) + const tx = await stakedToken.withdraw(withdrawAmount, sa.default.address, true, false) + await expect(tx).to.emit(stakedToken, "Withdraw").withArgs(sa.default.address, sa.default.address, withdrawAmount) + await expect(tx).to.emit(rewardToken, "Transfer").withArgs(stakedToken.address, sa.default.address, withdrawAmount.mul(3).div(4)) + + const afterData = await snapshotUserStakingData(sa.default.address) + expect(afterData.stakedBalance, "staker stkRWD after").to.eq(stakedAmount.sub(withdrawAmount)) + expect(afterData.votes, "staker votes after").to.eq(stakedAmount.sub(withdrawAmount)) + expect(afterData.cooldownTimestamp, "staked cooldown start").to.eq(0) + expect(afterData.cooldownPercentage, "staked cooldown percentage").to.eq(0) + expect(afterData.userBalances.cooldownMultiplier, "cooldown multiplier after").to.eq(0) + expect(afterData.userBalances.raw, "staked raw balance after").to.eq(stakedAmount.sub(withdrawAmount)) + }) + }) + }) + context("backward compatibility", () => { + const stakedAmount = simpleToExactAmount(2000) + beforeEach(async () => { + stakedToken = await redeployStakedToken() + await rewardToken.connect(sa.default.signer).approve(stakedToken.address, stakedAmount.mul(2)) + }) + it("createLock", async () => { + const tx = await stakedToken.createLock(stakedAmount, ONE_WEEK.mul(12)) + + const stakedTimestamp = await getTimestamp() + + await expect(tx).to.emit(stakedToken, "Staked").withArgs(sa.default.address, stakedAmount, ZERO_ADDRESS) + await expect(tx).to.emit(stakedToken, "DelegateChanged").not + await expect(tx).to.emit(stakedToken, "DelegateVotesChanged").withArgs(sa.default.address, 0, stakedAmount) + await expect(tx).to.emit(rewardToken, "Transfer").withArgs(sa.default.address, stakedToken.address, stakedAmount) + + const afterData = await snapshotUserStakingData(sa.default.address) + + expect(afterData.cooldownTimestamp, "cooldown timestamp after").to.eq(0) + expect(afterData.cooldownPercentage, "cooldown percentage after").to.eq(0) + expect(afterData.userBalances.raw, "staked raw balance after").to.eq(stakedAmount) + expect(afterData.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) + expect(afterData.userBalances.lastAction, "last action after").to.eq(stakedTimestamp) + expect(afterData.userBalances.permMultiplier, "perm multiplier after").to.eq(0) + expect(afterData.userBalances.seasonMultiplier, "season multiplier after").to.eq(0) + expect(afterData.userBalances.timeMultiplier, "time multiplier after").to.eq(0) + expect(afterData.userBalances.cooldownMultiplier, "cooldown multiplier after").to.eq(0) + expect(afterData.stakedBalance, "staked balance after").to.eq(stakedAmount) + expect(afterData.votes, "staker votes after").to.eq(stakedAmount) + + expect(await stakedToken.totalSupply(), "total staked after").to.eq(stakedAmount) + }) + it("increaseLockAmount", async () => { + await stakedToken.createLock(stakedAmount, ONE_WEEK.mul(12)) + await increaseTime(ONE_WEEK.mul(10)) + const increaseAmount = simpleToExactAmount(200) + const newBalance = stakedAmount.add(increaseAmount) + const tx = await stakedToken.increaseLockAmount(increaseAmount) + + const increaseTimestamp = await getTimestamp() + + await expect(tx).to.emit(stakedToken, "Staked").withArgs(sa.default.address, increaseAmount, ZERO_ADDRESS) + await expect(tx).to.emit(stakedToken, "DelegateChanged").not + await expect(tx).to.emit(stakedToken, "DelegateVotesChanged").withArgs(sa.default.address, stakedAmount, newBalance) + await expect(tx).to.emit(rewardToken, "Transfer").withArgs(sa.default.address, stakedToken.address, increaseAmount) + + const afterData = await snapshotUserStakingData(sa.default.address) + + expect(afterData.cooldownTimestamp, "cooldown timestamp after").to.eq(0) + expect(afterData.cooldownPercentage, "cooldown percentage after").to.eq(0) + expect(afterData.userBalances.raw, "staked raw balance after").to.eq(newBalance) + // expect(afterData.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(increaseTimestamp) + expect(afterData.userBalances.lastAction, "last action after").to.eq(increaseTimestamp) + expect(afterData.userBalances.permMultiplier, "perm multiplier after").to.eq(0) + expect(afterData.userBalances.seasonMultiplier, "season multiplier after").to.eq(0) + expect(afterData.userBalances.timeMultiplier, "time multiplier after").to.eq(0) + expect(afterData.userBalances.cooldownMultiplier, "cooldown multiplier after").to.eq(0) + expect(afterData.stakedBalance, "staked balance after").to.eq(newBalance) + expect(afterData.votes, "staker votes after").to.eq(newBalance) + + expect(await stakedToken.totalSupply(), "total staked after").to.eq(newBalance) + }) + it("increase lock length", async () => { + await stakedToken.createLock(stakedAmount, ONE_WEEK.mul(12)) + const stakedTimestamp = await getTimestamp() + await increaseTime(ONE_WEEK.mul(10)) + + await stakedToken.increaseLockLength(ONE_WEEK.mul(20)) + + const afterData = await snapshotUserStakingData(sa.default.address) + + expect(afterData.cooldownTimestamp, "cooldown timestamp after").to.eq(0) + expect(afterData.cooldownPercentage, "cooldown percentage after").to.eq(0) + expect(afterData.userBalances.raw, "staked raw balance after").to.eq(stakedAmount) + expect(afterData.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) + expect(afterData.userBalances.lastAction, "last action after").to.eq(stakedTimestamp) + expect(afterData.userBalances.permMultiplier, "perm multiplier after").to.eq(0) + expect(afterData.userBalances.seasonMultiplier, "season multiplier after").to.eq(0) + expect(afterData.userBalances.timeMultiplier, "time multiplier after").to.eq(0) + expect(afterData.userBalances.cooldownMultiplier, "cooldown multiplier after").to.eq(0) + expect(afterData.stakedBalance, "staked balance after").to.eq(stakedAmount) + expect(afterData.votes, "staker votes after").to.eq(stakedAmount) + + expect(await stakedToken.totalSupply(), "total staked after").to.eq(stakedAmount) + + }) }) context("interacting from a smart contract", () => { let stakedTokenWrapper @@ -1500,7 +1639,6 @@ describe("Staked Token", () => { const stakedAmount = simpleToExactAmount(10000) beforeEach(async () => { stakedToken = await redeployStakedToken() - await nexus.setRecollateraliser(sa.mockRecollateraliser.address) const users = [sa.default, sa.dummy1, sa.dummy2, sa.dummy3, sa.dummy4] for (const user of users) { await rewardToken.transfer(user.address, stakedAmount) From 80e3112b5e7ece69f15182d7ca013c1081be6293 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 20 Aug 2021 16:52:51 +1000 Subject: [PATCH 08/10] chore: more staked token tests --- test/governance/staked-token.spec.ts | 106 +++++++++++++++++++++++---- 1 file changed, 90 insertions(+), 16 deletions(-) diff --git a/test/governance/staked-token.spec.ts b/test/governance/staked-token.spec.ts index c622d6b4..3aece696 100644 --- a/test/governance/staked-token.spec.ts +++ b/test/governance/staked-token.spec.ts @@ -924,6 +924,7 @@ describe("Staked Token", () => { await rewardToken.connect(sa.default.signer).approve(stakedToken.address, simpleToExactAmount(10000)) await stakedToken["stake(uint256,address)"](stakedAmount, sa.default.address) stakedTimestamp = await getTimestamp() + await increaseTime(ONE_WEEK.mul(2)) }) context("should fail when", () => { it("nothing staked", async () => { @@ -948,8 +949,7 @@ describe("Staked Token", () => { expect(stakerDataAfter.cooldownTimestamp, "cooldown timestamp after").to.eq(startCooldownTimestamp) expect(stakerDataAfter.cooldownPercentage, "cooldown percentage after").to.eq(cooldown100Percentage) expect(stakerDataAfter.userBalances.raw, "staked raw balance after").to.eq(stakedAmount) - // TODO why is weightedTimestamp 1 second behind lastAction? - expect(stakerDataAfter.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(startCooldownTimestamp.sub(1)) + expect(stakerDataAfter.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(stakedTimestamp) expect(stakerDataAfter.userBalances.lastAction, "last action after").to.eq(startCooldownTimestamp) expect(stakerDataAfter.userBalances.permMultiplier, "perm multiplier after").to.eq(0) expect(stakerDataAfter.userBalances.seasonMultiplier, "season multiplier after").to.eq(0) @@ -972,9 +972,7 @@ describe("Staked Token", () => { expect(stakerDataAfter1stooldown.cooldownTimestamp, "cooldown timestamp after 1st").to.eq(cooldown1stTimestamp) expect(stakerDataAfter1stooldown.cooldownPercentage, "cooldown percentage after 1st").to.eq(firstCooldown) expect(stakerDataAfter1stooldown.userBalances.raw, "staked raw balance after 1st").to.eq(stakedAmount) - expect(stakerDataAfter1stooldown.userBalances.weightedTimestamp, "weighted timestamp after 1st").to.eq( - cooldown1stTimestamp.sub(1), - ) + expect(stakerDataAfter1stooldown.userBalances.weightedTimestamp, "weighted timestamp after 1st").to.eq(stakedTimestamp) expect(stakerDataAfter1stooldown.userBalances.lastAction, "last action after 1st").to.eq(cooldown1stTimestamp) expect(stakerDataAfter1stooldown.userBalances.permMultiplier, "perm multiplier after 1st").to.eq(0) expect(stakerDataAfter1stooldown.userBalances.seasonMultiplier, "season multiplier after 1st").to.eq(0) @@ -1340,7 +1338,9 @@ describe("Staked Token", () => { // fee = staked withdraw * 0.1 // fee = withdraw with fee / 1.1 * 0.1 = withdraw with fee / 11 const redemptionFee = stakedAmount.div(11) + const tx2 = await stakedToken.withdraw(stakedAmount, sa.default.address, true, true) + await expect(tx2).to.emit(stakedToken, "Withdraw").withArgs(sa.default.address, sa.default.address, stakedAmount) const afterData = await snapshotUserStakingData(sa.default.address) @@ -1350,9 +1350,6 @@ describe("Staked Token", () => { expect(afterData.cooldownPercentage, "staked cooldown percentage").to.eq(0) expect(afterData.userBalances.cooldownMultiplier, "cooldown multiplier after").to.eq(0) expect(afterData.userBalances.raw, "staked raw balance after").to.eq(0) - // expect(afterData.rewardsBalance, "staker rewards after").to.eq( - // beforeData.rewardsBalance.add(stakedAmount).sub(redemptionFee), - // ) assertBNClose(afterData.rewardsBalance, beforeData.rewardsBalance.add(stakedAmount).sub(redemptionFee), 1) }) it("not reset the cooldown timer unless all is all unstaked") @@ -1395,8 +1392,8 @@ describe("Staked Token", () => { console.log("data before") console.log(beforeData.cooldownPercentage.toString(), beforeData.userBalances.raw.toString()) console.log(afterData.cooldownPercentage.toString(), afterData.userBalances.raw.toString()) - // expect(afterData.stakedBalance, "staker staked after").to.eq(remainingBalance) - // expect(afterData.votes, "staker votes after").to.eq(remainingBalance) + // TODO expect(afterData.stakedBalance, "staker staked after").to.eq(remainingBalance) + // TODO expect(afterData.votes, "staker votes after").to.eq(remainingBalance) expect(afterData.cooldownTimestamp, "cooldown timestamp after").to.eq(beforeData.cooldownTimestamp) // 1400 / 2000 * 100 = 70 // (1400 - 300 - 30) / (2000 - 300 - 30) * 1e18 = 64.0718563e16 @@ -1427,9 +1424,6 @@ describe("Staked Token", () => { expect(afterData.cooldownPercentage, "staked cooldown percentage").to.eq(0) expect(afterData.userBalances.cooldownMultiplier, "cooldown multiplier after").to.eq(0) expect(afterData.userBalances.raw, "staked raw balance after").to.eq(stakedAmount.sub(cooldownAmount)) - // expect(afterData.rewardsBalance, "staker rewards after").to.eq( - // beforeData.rewardsBalance.add(cooldownAmount).sub(redemptionFee), - // ) assertBNClose(afterData.rewardsBalance, beforeData.rewardsBalance.add(cooldownAmount).sub(redemptionFee), 1) }) it("not reset the cooldown timer unless all is all unstaked") @@ -1449,7 +1443,9 @@ describe("Staked Token", () => { it("should withdraw all incl fee and get 75% of rewards", async () => { const tx = await stakedToken.withdraw(stakedAmount, sa.default.address, true, false) await expect(tx).to.emit(stakedToken, "Withdraw").withArgs(sa.default.address, sa.default.address, stakedAmount) - await expect(tx).to.emit(rewardToken, "Transfer").withArgs(stakedToken.address, sa.default.address, stakedAmount.mul(3).div(4)) + await expect(tx) + .to.emit(rewardToken, "Transfer") + .withArgs(stakedToken.address, sa.default.address, stakedAmount.mul(3).div(4)) const afterData = await snapshotUserStakingData(sa.default.address) expect(afterData.stakedBalance, "staker stkRWD after").to.eq(0) @@ -1462,7 +1458,9 @@ describe("Staked Token", () => { it("should withdraw all excl. fee and get 75% of rewards", async () => { const tx = await stakedToken.withdraw(stakedAmount, sa.default.address, false, false) await expect(tx).to.emit(stakedToken, "Withdraw").withArgs(sa.default.address, sa.default.address, stakedAmount) - await expect(tx).to.emit(rewardToken, "Transfer").withArgs(stakedToken.address, sa.default.address, stakedAmount.mul(3).div(4)) + await expect(tx) + .to.emit(rewardToken, "Transfer") + .withArgs(stakedToken.address, sa.default.address, stakedAmount.mul(3).div(4)) const afterData = await snapshotUserStakingData(sa.default.address) expect(afterData.stakedBalance, "staker stkRWD after").to.eq(0) @@ -1476,7 +1474,9 @@ describe("Staked Token", () => { const withdrawAmount = stakedAmount.div(10) const tx = await stakedToken.withdraw(withdrawAmount, sa.default.address, true, false) await expect(tx).to.emit(stakedToken, "Withdraw").withArgs(sa.default.address, sa.default.address, withdrawAmount) - await expect(tx).to.emit(rewardToken, "Transfer").withArgs(stakedToken.address, sa.default.address, withdrawAmount.mul(3).div(4)) + await expect(tx) + .to.emit(rewardToken, "Transfer") + .withArgs(stakedToken.address, sa.default.address, withdrawAmount.mul(3).div(4)) const afterData = await snapshotUserStakingData(sa.default.address) expect(afterData.stakedBalance, "staker stkRWD after").to.eq(stakedAmount.sub(withdrawAmount)) @@ -1487,6 +1487,32 @@ describe("Staked Token", () => { expect(afterData.userBalances.raw, "staked raw balance after").to.eq(stakedAmount.sub(withdrawAmount)) }) }) + context("calc redemption fee", () => { + let currentTime: BN + before(async () => { + stakedToken = await redeployStakedToken() + currentTime = await getTimestamp() + }) + const runs = [ + { stakedSeconds: BN.from(0), expected: 100, desc: "immediate" }, + { stakedSeconds: ONE_DAY, expected: 100, desc: "1 day" }, + { stakedSeconds: ONE_WEEK, expected: 100, desc: "1 week" }, + { stakedSeconds: ONE_WEEK.mul(2), expected: 100, desc: "2 weeks" }, + { stakedSeconds: ONE_WEEK.mul(21).div(10), expected: 94.52286093, desc: "2.1 weeks" }, + { stakedSeconds: ONE_WEEK.mul(10), expected: 29.77225575, desc: "10 weeks" }, + { stakedSeconds: ONE_WEEK.mul(12), expected: 25, desc: "12 weeks" }, + { stakedSeconds: ONE_WEEK.mul(47), expected: 0.26455763, desc: "47 weeks" }, + { stakedSeconds: ONE_WEEK.mul(48), expected: 0, desc: "48 weeks" }, + { stakedSeconds: ONE_WEEK.mul(50), expected: 0, desc: "50 weeks" }, + ] + runs.forEach((run) => { + it(run.desc, async () => { + expect(await stakedToken.calcRedemptionFeeRate(currentTime.sub(run.stakedSeconds))).to.eq( + simpleToExactAmount(run.expected, 15), + ) + }) + }) + }) }) context("backward compatibility", () => { const stakedAmount = simpleToExactAmount(2000) @@ -1572,7 +1598,55 @@ describe("Staked Token", () => { expect(afterData.votes, "staker votes after").to.eq(stakedAmount) expect(await stakedToken.totalSupply(), "total staked after").to.eq(stakedAmount) + }) + it("first exit", async () => { + await stakedToken.createLock(stakedAmount, ONE_WEEK.mul(20)) + const stakeTimestamp = await getTimestamp() + await increaseTime(ONE_WEEK.mul(18)) + + const tx = await stakedToken.exit() + + await expect(tx).to.emit(stakedToken, "Cooldown").withArgs(sa.default.address, cooldown100Percentage) + + const startCooldownTimestamp = await getTimestamp() + const stakerDataAfter = await snapshotUserStakingData(sa.default.address) + expect(stakerDataAfter.cooldownTimestamp, "cooldown timestamp after").to.eq(startCooldownTimestamp) + expect(stakerDataAfter.cooldownPercentage, "cooldown percentage after").to.eq(cooldown100Percentage) + expect(stakerDataAfter.userBalances.raw, "staked raw balance after").to.eq(stakedAmount) + expect(stakerDataAfter.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(stakeTimestamp) + expect(stakerDataAfter.userBalances.lastAction, "last action after").to.eq(startCooldownTimestamp) + expect(stakerDataAfter.userBalances.permMultiplier, "perm multiplier after").to.eq(0) + expect(stakerDataAfter.userBalances.seasonMultiplier, "season multiplier after").to.eq(0) + expect(stakerDataAfter.userBalances.timeMultiplier, "time multiplier after").to.eq(20) + expect(stakerDataAfter.userBalances.cooldownMultiplier, "cooldown multiplier after").to.eq(100) + expect(stakerDataAfter.stakedBalance, "staked balance after").to.eq(0) + expect(stakerDataAfter.votes, "votes after").to.eq(0) + }) + it("second exit", async () => { + await stakedToken.createLock(stakedAmount, ONE_WEEK.mul(20)) + await increaseTime(ONE_DAY.mul(1)) + await stakedToken.exit() + await increaseTime(ONE_DAY.mul(8)) + + const tx = await stakedToken.exit() + const redemptionFee = stakedAmount.div(11).add(1) + await expect(tx).to.emit(stakedToken, "Withdraw").withArgs(sa.default.address, sa.default.address, stakedAmount) + await expect(tx) + .to.emit(rewardToken, "Transfer") + .withArgs(stakedToken.address, sa.default.address, stakedAmount.sub(redemptionFee)) + + const withdrawTimestamp = await getTimestamp() + const afterData = await snapshotUserStakingData(sa.default.address) + expect(afterData.stakedBalance, "staker stkRWD after").to.eq(0) + expect(afterData.votes, "staker votes after").to.eq(0) + expect(afterData.cooldownTimestamp, "staked cooldown start").to.eq(0) + expect(afterData.cooldownPercentage, "staked cooldown percentage").to.eq(0) + expect(afterData.userBalances.cooldownMultiplier, "cooldown multiplier after").to.eq(0) + expect(afterData.userBalances.raw, "staked raw balance after").to.eq(0) + // TODO expect(afterData.userBalances.weightedTimestamp, "weighted timestamp after").to.eq(withdrawTimestamp) + expect(afterData.userBalances.lastAction, "last action after").to.eq(withdrawTimestamp) + expect(afterData.rewardsBalance, "staker rewards after").to.eq(startingMintAmount.sub(redemptionFee)) }) }) context("interacting from a smart contract", () => { From 1571841ee43530c8ac1f95ac0dbd26acec314cdf Mon Sep 17 00:00:00 2001 From: alsco77 Date: Fri, 20 Aug 2021 15:24:27 +0100 Subject: [PATCH 09/10] chore: Tweak tests for increasLockAmount --- contracts/governance/staking/StakedToken.sol | 2 +- test/governance/staked-token.spec.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/contracts/governance/staking/StakedToken.sol b/contracts/governance/staking/StakedToken.sol index bfa9e5f7..20093145 100644 --- a/contracts/governance/staking/StakedToken.sol +++ b/contracts/governance/staking/StakedToken.sol @@ -425,7 +425,7 @@ contract StakedToken is IStakedToken, GamifiedVotingToken { * @param _value Units to stake **/ function increaseLockAmount(uint256 _value) external { - require(balanceOf(_msgSender) == 0, "Nothing to increase"); + require(balanceOf(_msgSender()) != 0, "Nothing to increase"); _transferAndStake(_value, address(0), false); } diff --git a/test/governance/staked-token.spec.ts b/test/governance/staked-token.spec.ts index 058cbf4a..86214c5f 100644 --- a/test/governance/staked-token.spec.ts +++ b/test/governance/staked-token.spec.ts @@ -1573,6 +1573,20 @@ describe("Staked Token", () => { expect(await stakedToken.totalSupply(), "total staked after").to.eq(newBalance) }) + it("fails to increaseLockAmount if the user has no stake", async () => { + // Fresh slate, fail + await expect(stakedToken.increaseLockAmount(stakedAmount)).to.revertedWith("Nothing to increase") + + // Stake, withdraw, fail + await stakedToken.createLock(stakedAmount, ONE_WEEK.mul(12)) + await stakedToken.startCooldown(stakedAmount) + await increaseTime(ONE_DAY.mul(8)) + await stakedToken.withdraw(stakedAmount, sa.default.address, true, false) + const data = await snapshotUserStakingData() + expect(data.stakedBalance).eq(BN.from(0)) + expect(data.cooldownTimestamp).eq(BN.from(0)) + await expect(stakedToken.increaseLockAmount(stakedAmount)).to.revertedWith("Nothing to increase") + }) it("increase lock length", async () => { await stakedToken.createLock(stakedAmount, ONE_WEEK.mul(12)) const stakedTimestamp = await getTimestamp() From b88c57fbe00a77f0221915507981bb1ad4278864 Mon Sep 17 00:00:00 2001 From: alsco77 Date: Fri, 20 Aug 2021 16:11:32 +0100 Subject: [PATCH 10/10] fix: Support withdrawal after recollateralisation --- contracts/governance/staking/GamifiedToken.sol | 12 ++++++++++-- contracts/governance/staking/StakedToken.sol | 4 ++-- test/governance/staked-token.spec.ts | 8 ++++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/contracts/governance/staking/GamifiedToken.sol b/contracts/governance/staking/GamifiedToken.sol index b125b409..42cbb676 100644 --- a/contracts/governance/staking/GamifiedToken.sol +++ b/contracts/governance/staking/GamifiedToken.sol @@ -486,11 +486,13 @@ abstract contract GamifiedToken is * @param _account Address of user * @param _rawAmount Raw amount of tokens to remove * @param _exitCooldown Exit the cooldown? + * @param _finalise Has recollateralisation happened? If so, everything is cooled down */ function _burnRaw( address _account, uint256 _rawAmount, - bool _exitCooldown + bool _exitCooldown, + bool _finalise ) internal virtual updateReward(_account) { require(_account != address(0), "ERC20: burn from zero address"); @@ -498,7 +500,13 @@ abstract contract GamifiedToken is (Balance memory oldBalance, uint256 oldScaledBalance) = _prepareOldBalance(_account); CooldownData memory cooldownData = stakersCooldowns[_msgSender()]; uint256 totalRaw = oldBalance.raw + cooldownData.units; - // 1.1 Update + // 1.1. If _finalise, move everything to cooldown + if (_finalise) { + _balances[_account].raw = 0; + stakersCooldowns[_account].units = SafeCast.toUint128(totalRaw); + cooldownData.units = SafeCast.toUint128(totalRaw); + } + // 1.2. Update require(cooldownData.units >= _rawAmount, "ERC20: burn amount > balance"); unchecked { stakersCooldowns[_account].units -= SafeCast.toUint128(_rawAmount); diff --git a/contracts/governance/staking/StakedToken.sol b/contracts/governance/staking/StakedToken.sol index 20093145..d6cb752f 100644 --- a/contracts/governance/staking/StakedToken.sol +++ b/contracts/governance/staking/StakedToken.sol @@ -252,7 +252,7 @@ contract StakedToken is IStakedToken, GamifiedVotingToken { // Is the contract post-recollateralisation? if (safetyData.collateralisationRatio != 1e18) { // 1. If recollateralisation has occured, the contract is finished and we can skip all checks - _burnRaw(_msgSender(), _amount, false); + _burnRaw(_msgSender(), _amount, false, true); // 2. Return a proportionate amount of tokens, based on the collateralisation ratio STAKED_TOKEN.safeTransfer( _recipient, @@ -293,7 +293,7 @@ contract StakedToken is IStakedToken, GamifiedVotingToken { bool exitCooldown = _exitCooldown || totalWithdraw == maxWithdrawal; // 5. Settle the withdrawal by burning the voting tokens - _burnRaw(_msgSender(), totalWithdraw, exitCooldown); + _burnRaw(_msgSender(), totalWithdraw, exitCooldown, false); // Log any redemption fee to the rewards contract _notifyAdditionalReward(totalWithdraw - userWithdrawal); // Finally transfer tokens back to recipient diff --git a/test/governance/staked-token.spec.ts b/test/governance/staked-token.spec.ts index 86214c5f..7bb3f6c2 100644 --- a/test/governance/staked-token.spec.ts +++ b/test/governance/staked-token.spec.ts @@ -1479,11 +1479,11 @@ describe("Staked Token", () => { .withArgs(stakedToken.address, sa.default.address, withdrawAmount.mul(3).div(4)) const afterData = await snapshotUserStakingData(sa.default.address) - expect(afterData.stakedBalance, "staker stkRWD after").to.eq(stakedAmount.sub(withdrawAmount)) - expect(afterData.votes, "staker votes after").to.eq(stakedAmount.sub(withdrawAmount)) + expect(afterData.stakedBalance, "staker stkRWD after").to.eq(0) + expect(afterData.votes, "staker votes after").to.eq(0) expect(afterData.cooldownTimestamp, "staked cooldown start").to.eq(0) - expect(afterData.cooldownUnits, "staked cooldown units").to.eq(0) - expect(afterData.userBalances.raw, "staked raw balance after").to.eq(stakedAmount.sub(withdrawAmount)) + expect(afterData.cooldownUnits, "staked cooldown units").to.eq(stakedAmount.sub(withdrawAmount)) + expect(afterData.userBalances.raw, "staked raw balance after").to.eq(0) }) }) context("calc redemption fee", () => {