From e5ac92bddbb0158d9fb2b27b528f080cb94a4964 Mon Sep 17 00:00:00 2001 From: Tarrence van As Date: Sat, 18 Sep 2021 09:09:15 -0700 Subject: [PATCH] tests(dopedao): add lifecycle integration tests (#13) * oz bravo lifecycle * test compare executing with eth * remove console log * update tests * fix: upstream oz fix taken from https://github.com/OpenZeppelin/openzeppelin-contracts/commit/01f2ff1ba1220d7d3a0642014d30543bfcbb41ee Co-authored-by: Dennison Bertram --- contracts/governance/DopeDAO.sol | 24 +- contracts/governance/Timelock.sol | 382 +++++++++++++++++++++++++++++ contracts/test/DopeDAO.sol | 29 +++ contracts/test/Receiver.sol | 7 + hardhat.config.ts | 35 ++- package.json | 2 +- test/governance/DopeDAO.ts | 144 +++++++++++ test/nftStake/NftStake.behavior.ts | 108 ++++---- test/nftStake/NftStake.ts | 14 +- test/types.ts | 10 +- yarn.lock | 10 +- 11 files changed, 677 insertions(+), 88 deletions(-) create mode 100644 contracts/governance/Timelock.sol create mode 100644 contracts/test/DopeDAO.sol create mode 100644 contracts/test/Receiver.sol create mode 100644 test/governance/DopeDAO.ts diff --git a/contracts/governance/DopeDAO.sol b/contracts/governance/DopeDAO.sol index 64f5099..5b69d87 100644 --- a/contracts/governance/DopeDAO.sol +++ b/contracts/governance/DopeDAO.sol @@ -13,19 +13,19 @@ contract DopeDAO is Governor, GovernorCompatibilityBravo, GovernorVotesComp, Gov GovernorTimelockCompound(_timelock) {} - function votingDelay() public pure override returns (uint256) { + function votingDelay() public pure virtual override returns (uint256) { return 13091; // 2 days (in blocks) } - function votingPeriod() public pure override returns (uint256) { + function votingPeriod() public pure virtual override returns (uint256) { return 45818; // 1 week (in blocks) } - function quorum(uint256 blockNumber) public pure override returns (uint256) { + function quorum(uint256 blockNumber) public pure virtual override returns (uint256) { return 500; // DOPE DAO NFT TOKENS } - function proposalThreshold() public pure override returns (uint256) { + function proposalThreshold() public pure virtual override returns (uint256) { return 50; // DOPE DAO NFT TOKENS } @@ -63,9 +63,14 @@ contract DopeDAO is Governor, GovernorCompatibilityBravo, GovernorVotesComp, Gov address[] memory targets, uint256[] memory values, bytes[] memory calldatas, - bytes32 descriptionHash + bytes32 /*descriptionHash*/ ) internal override(Governor, GovernorTimelockCompound) { - super._execute(proposalId, targets, values, calldatas, descriptionHash); + uint256 eta = proposalEta(proposalId); + require(eta > 0, "GovernorTimelockCompound: proposal not yet queued"); + Address.sendValue(payable(timelock()), msg.value); + for (uint256 i = 0; i < targets.length; ++i) { + ICompoundTimelock(payable(timelock())).executeTransaction(targets[i], values[i], "", calldatas[i], eta); + } } function _cancel( @@ -89,4 +94,11 @@ contract DopeDAO is Governor, GovernorCompatibilityBravo, GovernorVotesComp, Gov { return super.supportsInterface(interfaceId); } + + /** + * @dev Function to receive ETH that will be handled by the governor (disabled if executor is a third party contract) + */ + receive() external payable virtual { + require(_executor() == address(this)); + } } diff --git a/contracts/governance/Timelock.sol b/contracts/governance/Timelock.sol new file mode 100644 index 0000000..cad8096 --- /dev/null +++ b/contracts/governance/Timelock.sol @@ -0,0 +1,382 @@ +/** + *Submitted for verification at Etherscan.io on 2021-09-01 + */ + +// Sources flattened with hardhat v2.6.1 https://hardhat.org + +// File contracts/SafeMath.sol + +pragma solidity ^0.5.16; + +// From https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/math/Math.sol +// Subject to the MIT license. + +/** + * @dev Wrappers over Solidity's arithmetic operations with added overflow + * checks. + * + * Arithmetic operations in Solidity wrap on overflow. This can easily result + * in bugs, because programmers usually assume that an overflow raises an + * error, which is the standard behavior in high level programming languages. + * `SafeMath` restores this intuition by reverting the transaction when an + * operation overflows. + * + * Using this library instead of the unchecked operations eliminates an entire + * class of bugs, so it's recommended to use it always. + */ +library SafeMath { + /** + * @dev Returns the addition of two unsigned integers, reverting on overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * - Addition cannot overflow. + */ + function add(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a, "SafeMath: addition overflow"); + + return c; + } + + /** + * @dev Returns the addition of two unsigned integers, reverting with custom message on overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * - Addition cannot overflow. + */ + function add( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a, errorMessage); + + return c; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting on underflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * - Subtraction cannot underflow. + */ + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + return sub(a, b, "SafeMath: subtraction underflow"); + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting with custom message on underflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * - Subtraction cannot underflow. + */ + function sub( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + require(b <= a, errorMessage); + uint256 c = a - b; + + return c; + } + + /** + * @dev Returns the multiplication of two unsigned integers, reverting on overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * - Multiplication cannot overflow. + */ + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) { + return 0; + } + + uint256 c = a * b; + require(c / a == b, "SafeMath: multiplication overflow"); + + return c; + } + + /** + * @dev Returns the multiplication of two unsigned integers, reverting on overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * - Multiplication cannot overflow. + */ + function mul( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) { + return 0; + } + + uint256 c = a * b; + require(c / a == b, errorMessage); + + return c; + } + + /** + * @dev Returns the integer division of two unsigned integers. + * Reverts on division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function div(uint256 a, uint256 b) internal pure returns (uint256) { + return div(a, b, "SafeMath: division by zero"); + } + + /** + * @dev Returns the integer division of two unsigned integers. + * Reverts with custom message on division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function div( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + // Solidity only automatically asserts when dividing by 0 + require(b > 0, errorMessage); + uint256 c = a / b; + // assert(a == b * c + a % b); // There is no case in which this doesn't hold + + return c; + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function mod(uint256 a, uint256 b) internal pure returns (uint256) { + return mod(a, b, "SafeMath: modulo by zero"); + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts with custom message when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function mod( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + require(b != 0, errorMessage); + return a % b; + } +} + +// File contracts/Timelock.sol + +pragma solidity ^0.5.16; + +contract Timelock { + using SafeMath for uint256; + + event NewAdmin(address indexed newAdmin); + event NewPendingAdmin(address indexed newPendingAdmin); + event NewDelay(uint256 indexed newDelay); + event CancelTransaction( + bytes32 indexed txHash, + address indexed target, + uint256 value, + string signature, + bytes data, + uint256 eta + ); + event ExecuteTransaction( + bytes32 indexed txHash, + address indexed target, + uint256 value, + string signature, + bytes data, + uint256 eta + ); + event QueueTransaction( + bytes32 indexed txHash, + address indexed target, + uint256 value, + string signature, + bytes data, + uint256 eta + ); + + uint256 public constant GRACE_PERIOD = 14 days; + uint256 public constant MINIMUM_DELAY = 1 seconds; + uint256 public constant MAXIMUM_DELAY = 30 days; + + address public admin; + address public pendingAdmin; + uint256 public delay; + + mapping(bytes32 => bool) public queuedTransactions; + + constructor(address admin_, uint256 delay_) public { + require(delay_ >= MINIMUM_DELAY, "Timelock::constructor: Delay must exceed minimum delay."); + require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay."); + + admin = admin_; + delay = delay_; + } + + function() external payable {} + + function setDelay(uint256 delay_) public { + require(msg.sender == address(this), "Timelock::setDelay: Call must come from Timelock."); + require(delay_ >= MINIMUM_DELAY, "Timelock::setDelay: Delay must exceed minimum delay."); + require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay."); + delay = delay_; + + emit NewDelay(delay); + } + + function acceptAdmin() public { + require(msg.sender == pendingAdmin, "Timelock::acceptAdmin: Call must come from pendingAdmin."); + admin = msg.sender; + pendingAdmin = address(0); + + emit NewAdmin(admin); + } + + function setPendingAdmin(address pendingAdmin_) public { + require(msg.sender == address(this), "Timelock::setPendingAdmin: Call must come from Timelock."); + pendingAdmin = pendingAdmin_; + + emit NewPendingAdmin(pendingAdmin); + } + + function queueTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) public returns (bytes32) { + require(msg.sender == admin, "Timelock::queueTransaction: Call must come from admin."); + require( + eta >= getBlockTimestamp().add(delay), + "Timelock::queueTransaction: Estimated execution block must satisfy delay." + ); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + queuedTransactions[txHash] = true; + + emit QueueTransaction(txHash, target, value, signature, data, eta); + return txHash; + } + + function cancelTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) public { + require(msg.sender == admin, "Timelock::cancelTransaction: Call must come from admin."); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + queuedTransactions[txHash] = false; + + emit CancelTransaction(txHash, target, value, signature, data, eta); + } + + function executeTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) public payable returns (bytes memory) { + require(msg.sender == admin, "Timelock::executeTransaction: Call must come from admin."); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + require(queuedTransactions[txHash], "Timelock::executeTransaction: Transaction hasn't been queued."); + require(getBlockTimestamp() >= eta, "Timelock::executeTransaction: Transaction hasn't surpassed time lock."); + require(getBlockTimestamp() <= eta.add(GRACE_PERIOD), "Timelock::executeTransaction: Transaction is stale."); + + queuedTransactions[txHash] = false; + + bytes memory callData; + + if (bytes(signature).length == 0) { + callData = data; + } else { + callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data); + } + + // solium-disable-next-line security/no-call-value + (bool success, bytes memory returnData) = target.call.value(value)(callData); + require(success, "Timelock::executeTransaction: Transaction execution reverted."); + + emit ExecuteTransaction(txHash, target, value, signature, data, eta); + + return returnData; + } + + function getBlockTimestamp() internal view returns (uint256) { + // solium-disable-next-line security/no-block-members + return block.timestamp; + } +} + +// File contracts/Mock.sol + +pragma solidity ^0.5.16; + +contract Mock { + event Received(string message, uint256 ethAmount); + + string public message; + + function receiveETH(string memory _message) public payable { + message = _message; + emit Received(_message, msg.value); + } +} diff --git a/contracts/test/DopeDAO.sol b/contracts/test/DopeDAO.sol new file mode 100644 index 0000000..3c86cdd --- /dev/null +++ b/contracts/test/DopeDAO.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "@openzeppelin/contracts/governance/Governor.sol"; +import "@openzeppelin/contracts/governance/compatibility/GovernorCompatibilityBravo.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorVotesComp.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorTimelockCompound.sol"; + +import "../governance/DopeDAO.sol"; + +contract DopeDAOTest is DopeDAO { + constructor(ERC20VotesComp _token, ICompoundTimelock _timelock) DopeDAO(_token, _timelock) {} + + function votingDelay() public pure override returns (uint256) { + return 1; + } + + function votingPeriod() public pure override returns (uint256) { + return 2; + } + + function quorum(uint256 blockNumber) public pure override returns (uint256) { + return 1; + } + + function proposalThreshold() public pure override returns (uint256) { + return 1; + } +} diff --git a/contracts/test/Receiver.sol b/contracts/test/Receiver.sol new file mode 100644 index 0000000..6a2e72d --- /dev/null +++ b/contracts/test/Receiver.sol @@ -0,0 +1,7 @@ +pragma solidity ^0.8.2; + +contract Receiver { + function receiveEth(string calldata) public payable {} + + function receiveNoEth(string calldata) public {} +} diff --git a/hardhat.config.ts b/hardhat.config.ts index dcc886e..86e2129 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -61,20 +61,31 @@ const config: HardhatUserConfig = { tests: "./test", }, solidity: { - version: "0.8.6", - settings: { - metadata: { - // Not including the metadata hash - // https://github.com/paulrberg/solidity-template/issues/31 - bytecodeHash: "none", - }, - // Disable the optimizer when debugging - // https://hardhat.org/hardhat-network/#solidity-optimizer-support - optimizer: { - enabled: true, - runs: 800, + compilers: [{ + version: "0.8.6", + settings: { + metadata: { + // Not including the metadata hash + // https://github.com/paulrberg/solidity-template/issues/31 + bytecodeHash: "none", + }, + // Disable the optimizer when debugging + // https://hardhat.org/hardhat-network/#solidity-optimizer-support + optimizer: { + enabled: true, + runs: 800, + }, }, }, + { + version: '0.5.17', + settings: { + optimizer: { + enabled: true, + runs: 100, + }, + }, + }], }, typechain: { outDir: "typechain", diff --git a/package.json b/package.json index 0a04cfe..261f0d2 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,6 @@ }, "dependencies": { "@nomiclabs/hardhat-etherscan": "^2.1.6", - "@openzeppelin/contracts": ">=4.3.1" + "@openzeppelin/contracts": "^4.3.2" } } diff --git a/test/governance/DopeDAO.ts b/test/governance/DopeDAO.ts new file mode 100644 index 0000000..70a78f6 --- /dev/null +++ b/test/governance/DopeDAO.ts @@ -0,0 +1,144 @@ +import hre, { ethers } from "hardhat"; +import { expect } from "chai"; + +import { Artifact } from "hardhat/types"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; + +import { Signers } from "../types"; +import { DopeWarsLoot, DopeDAOTest, Receiver, Timelock } from "../../typechain"; + +const { deployContract } = hre.waffle; + +describe("DopeDAO", function () { + before(async function () { + this.signers = {} as Signers; + + const signers: SignerWithAddress[] = await hre.ethers.getSigners(); + this.signers.admin = signers[0]; + this.signers.alice = signers[1]; + this.signers.bob = signers[2]; + }); + + describe("lifecycle", function () { + beforeEach(async function () { + const lootArtifact: Artifact = await hre.artifacts.readArtifact("DopeWarsLoot"); + this.loot = await deployContract(this.signers.admin, lootArtifact, []); + + const timelockArtifact: Artifact = await hre.artifacts.readArtifact("Timelock"); + this.timelock = await deployContract(this.signers.admin, timelockArtifact, [this.signers.admin.address, 10]); + + const daoArtifact: Artifact = await hre.artifacts.readArtifact("DopeDAOTest"); + this.dao = await deployContract(this.signers.admin, daoArtifact, [this.loot.address, this.timelock.address]); + + const receiverArtifact: Artifact = await hre.artifacts.readArtifact("Receiver"); + this.receiver = await deployContract(this.signers.admin, receiverArtifact, []); + + await Promise.all([...Array(5).keys()].map(async (i) => this.loot.claim(i + 1))) + }); + + it("propose and execute a proposal with no eth", async function () { + let now = await hre.waffle.provider.getBlock('latest').then(block => block.timestamp) + const eta = now + 11; + const sig = "setPendingAdmin(address)" + const data = new ethers.utils.AbiCoder().encode(["address"], [this.dao.address]); + + await this.timelock.queueTransaction(this.timelock.address, 0, sig, data, eta); + + await hre.network.provider.request({ + method: "evm_setNextBlockTimestamp", + params: [eta], + }); + + await this.timelock.executeTransaction(this.timelock.address, 0, sig, data, eta); + await this.dao.__acceptAdmin() + + const calldata = new ethers.utils.AbiCoder().encode(["string"], ["gang"]); + + const txn = await this.dao["propose(address[],uint256[],string[],bytes[],string)"]( + [this.receiver.address], [0], ["receiveNoEth(string)"], [calldata], "Send no ETH" + ) + + const receipt = await txn.wait() + const proposalId = receipt.events![0].args!.proposalId + + // check proposal id exists + expect((await this.dao.proposals(proposalId)).forVotes.toString()).to.eql("0") + + await hre.network.provider.send("evm_mine"); + + await this.dao.castVote(proposalId, 1); + + // check we have voted + expect((await this.dao.proposals(proposalId)).forVotes.toString()).to.eql("5") + + await this.dao["queue(uint256)"](proposalId); + + now = await hre.waffle.provider.getBlock('latest').then(block => block.timestamp) + await hre.network.provider.request({ + method: "evm_setNextBlockTimestamp", + params: [now + 11], + }); + + await this.dao["execute(uint256)"](proposalId) + + // check it executed + expect((await this.dao.proposals(proposalId)).executed).to.eql(true); + }) + + it("propose and execute a proposal with eth", async function () { + let now = await hre.waffle.provider.getBlock('latest').then(block => block.timestamp) + const eta = now + 12; + const sig = "setPendingAdmin(address)" + const data = new ethers.utils.AbiCoder().encode(["address"], [this.dao.address]); + + const value = ethers.utils.parseEther("0.1") + // send eth to the timelock + await this.signers.alice.sendTransaction({ + to: this.timelock.address, + value + }) + + await this.timelock.queueTransaction(this.timelock.address, 0, sig, data, eta) + + await hre.network.provider.request({ + method: "evm_setNextBlockTimestamp", + params: [eta], + }); + + await this.timelock.executeTransaction(this.timelock.address, 0, sig, data, eta) + await this.dao.__acceptAdmin() + + const calldata = new ethers.utils.AbiCoder().encode(["string"], ["gang"]); + + const txn = await this.dao["propose(address[],uint256[],string[],bytes[],string)"]( + [this.receiver.address], [value], ["receiveEth(string)"], [calldata], "Send ETH" + ) + + const receipt = await txn.wait() + const proposalId = receipt.events![0].args!.proposalId + + // check proposal id exists + expect((await this.dao.proposals(proposalId)).forVotes.toString()).to.eql("0") + + await hre.network.provider.send("evm_mine"); + + await this.dao.castVote(proposalId, 1); + + // check we have voted + expect((await this.dao.proposals(proposalId)).forVotes.toString()).to.eql("5") + + await this.dao["queue(uint256)"](proposalId); + + now = await hre.waffle.provider.getBlock('latest').then(block => block.timestamp) + await hre.network.provider.request({ + method: "evm_setNextBlockTimestamp", + params: [now + 11], + }); + + await this.dao["execute(uint256)"](proposalId) + + // check it executed + expect((await this.dao.proposals(proposalId)).executed).to.eql(true); + }) + }) +}) diff --git a/test/nftStake/NftStake.behavior.ts b/test/nftStake/NftStake.behavior.ts index f5f80d4..6029c3c 100644 --- a/test/nftStake/NftStake.behavior.ts +++ b/test/nftStake/NftStake.behavior.ts @@ -5,40 +5,40 @@ import { network } from "hardhat"; export function shouldBehaveLikeNftStake(): void { it("should let user stake NFT", async function () { // Need to approve the token first - await expect(this.nftStake.connect(this.signers.user1).stake([BigNumber.from(1)], true)).to.be.revertedWith( + await expect(this.nftStake.connect(this.signers.alice).stake([BigNumber.from(1)], true)).to.be.revertedWith( "ERC721: transfer caller is not owner nor approved", ); // Approve nftStake to take the token - await this.mockERC721.connect(this.signers.user1).approve(this.nftStake.address, BigNumber.from(1)); + await this.mockERC721.connect(this.signers.alice).approve(this.nftStake.address, BigNumber.from(1)); // Try to stake it - await expect(this.nftStake.connect(this.signers.user1).stake([BigNumber.from(1)], true)).to.not.be.reverted; + await expect(this.nftStake.connect(this.signers.alice).stake([BigNumber.from(1)], true)).to.not.be.reverted; }); it("should not let a user stake twice", async function () { // Approve nftStake to take the token - await this.mockERC721.connect(this.signers.user1).approve(this.nftStake.address, BigNumber.from(1)); + await this.mockERC721.connect(this.signers.alice).approve(this.nftStake.address, BigNumber.from(1)); // Try to stake it - await expect(this.nftStake.connect(this.signers.user1).stake([BigNumber.from(1)], true)).to.not.be.reverted; + await expect(this.nftStake.connect(this.signers.alice).stake([BigNumber.from(1)], true)).to.not.be.reverted; // Try to stake again - await expect(this.nftStake.connect(this.signers.user1).stake([BigNumber.from(1)], true)).to.be.revertedWith( + await expect(this.nftStake.connect(this.signers.alice).stake([BigNumber.from(1)], true)).to.be.revertedWith( "ERC721: transfer of token that is not own", ); }); it("should not let you stake a token you don't own", async function () { // Try to stake it - await expect(this.nftStake.connect(this.signers.user2).stake([BigNumber.from(1)], true)).to.be.reverted; + await expect(this.nftStake.connect(this.signers.bob).stake([BigNumber.from(1)], true)).to.be.reverted; }); it("should let user unstake", async function () { const tokenId = BigNumber.from(1); // Approve nftStake to take the token - await this.mockERC721.connect(this.signers.user1).approve(this.nftStake.address, tokenId); + await this.mockERC721.connect(this.signers.alice).approve(this.nftStake.address, tokenId); - await expect(this.nftStake.connect(this.signers.user1).stake([tokenId], false)).to.be.revertedWith("nftstake: must accept terms of service"); + await expect(this.nftStake.connect(this.signers.alice).stake([tokenId], false)).to.be.revertedWith("nftstake: must accept terms of service"); // Try to stake it - await expect(this.nftStake.connect(this.signers.user1).stake([tokenId], true)).to.not.be.reverted; + await expect(this.nftStake.connect(this.signers.alice).stake([tokenId], true)).to.not.be.reverted; // confirm nftStake owns token expect(await this.mockERC721.connect(this.signers.admin).ownerOf(tokenId)).to.eql(this.nftStake.address); @@ -55,27 +55,27 @@ export function shouldBehaveLikeNftStake(): void { // estimate stake const estimatedPayout = (currentBlock - startStake) * - (await (await this.nftStake.connect(this.signers.user1).emissionRate()).toNumber()); + (await (await this.nftStake.connect(this.signers.alice).emissionRate()).toNumber()); // check if estimated stake matches contract - expect(await (await this.nftStake.connect(this.signers.user1).rewardOf(tokenId)).toNumber()).to.eql( + expect(await (await this.nftStake.connect(this.signers.alice).rewardOf(tokenId)).toNumber()).to.eql( estimatedPayout, ); - await expect(this.nftStake.connect(this.signers.user1).unstake([tokenId], false)).to.be.revertedWith("nftstake: must accept terms of service"); + await expect(this.nftStake.connect(this.signers.alice).unstake([tokenId], false)).to.be.revertedWith("nftstake: must accept terms of service"); // try to unstake - await expect(this.nftStake.connect(this.signers.user1).unstake([tokenId], true)).to.not.be.reverted; + await expect(this.nftStake.connect(this.signers.alice).unstake([tokenId], true)).to.not.be.reverted; - // confirm user1 owns the token again - expect(await this.mockERC721.connect(this.signers.user1).ownerOf(tokenId)).to.eql( - await this.signers.user1.getAddress(), + // confirm alice owns the token again + expect(await this.mockERC721.connect(this.signers.alice).ownerOf(tokenId)).to.eql( + await this.signers.alice.getAddress(), ); - // check if user1 has been paid the estimated stake + // check if alice has been paid the estimated stake expect( await ( - await this.mockERC20.connect(this.signers.user1).balanceOf(await this.signers.user1.getAddress()) + await this.mockERC20.connect(this.signers.alice).balanceOf(await this.signers.alice.getAddress()) ).toNumber(), ).to.eql(estimatedPayout); }); @@ -83,49 +83,49 @@ export function shouldBehaveLikeNftStake(): void { it("should let user unstake in same block", async function () { const tokenId = BigNumber.from(1); // Approve nftStake to take the token - await this.mockERC721.connect(this.signers.user1).approve(this.nftStake.address, tokenId); + await this.mockERC721.connect(this.signers.alice).approve(this.nftStake.address, tokenId); - await expect(this.nftStake.connect(this.signers.user1).stake([tokenId], false)).to.be.revertedWith("nftstake: must accept terms of service"); + await expect(this.nftStake.connect(this.signers.alice).stake([tokenId], false)).to.be.revertedWith("nftstake: must accept terms of service"); // Try to stake it - await expect(this.nftStake.connect(this.signers.user1).stake([tokenId], true)).to.not.be.reverted; + await expect(this.nftStake.connect(this.signers.alice).stake([tokenId], true)).to.not.be.reverted; // confirm nftStake owns token expect(await this.mockERC721.connect(this.signers.admin).ownerOf(tokenId)).to.eql(this.nftStake.address); // check if estimated stake matches contract - expect(await (await this.nftStake.connect(this.signers.user1).rewardOf(tokenId)).toNumber()).to.eql( + expect(await (await this.nftStake.connect(this.signers.alice).rewardOf(tokenId)).toNumber()).to.eql( 0, ); // try to unstake - await expect(this.nftStake.connect(this.signers.user1).unstake([tokenId], true)).to.not.be.reverted; + await expect(this.nftStake.connect(this.signers.alice).unstake([tokenId], true)).to.not.be.reverted; - // confirm user1 owns the token again - expect(await this.mockERC721.connect(this.signers.user1).ownerOf(tokenId)).to.eql( - await this.signers.user1.getAddress(), + // confirm alice owns the token again + expect(await this.mockERC721.connect(this.signers.alice).ownerOf(tokenId)).to.eql( + await this.signers.alice.getAddress(), ); - // check if user1 has been paid the estimated stake + // check if alice has been paid the estimated stake expect( await ( - await this.mockERC20.connect(this.signers.user1).balanceOf(await this.signers.user1.getAddress()) + await this.mockERC20.connect(this.signers.alice).balanceOf(await this.signers.alice.getAddress()) ).toNumber(), ).to.eql(0); }); it("rewardOf should return zero when not staked", async function () { const tokenId = BigNumber.from(9999); - expect((await this.nftStake.connect(this.signers.user1).rewardOf(tokenId)).toNumber()).to.eql(0); + expect((await this.nftStake.connect(this.signers.alice).rewardOf(tokenId)).toNumber()).to.eql(0); }); it("rewardOf should return correct stake amount currently", async function () { const tokenId = BigNumber.from(1); // Approve nftStake to take the token - await this.mockERC721.connect(this.signers.user1).approve(this.nftStake.address, tokenId); + await this.mockERC721.connect(this.signers.alice).approve(this.nftStake.address, tokenId); // Try to stake it - await expect(this.nftStake.connect(this.signers.user1).stake([tokenId], true)).to.not.be.reverted; + await expect(this.nftStake.connect(this.signers.alice).stake([tokenId], true)).to.not.be.reverted; // Wait 4 blocks await network.provider.send("evm_mine"); @@ -133,7 +133,7 @@ export function shouldBehaveLikeNftStake(): void { await network.provider.send("evm_mine"); await network.provider.send("evm_mine"); - expect((await this.nftStake.connect(this.signers.user1).rewardOf(tokenId)).toNumber()).to.eq( + expect((await this.nftStake.connect(this.signers.alice).rewardOf(tokenId)).toNumber()).to.eq( 4 * this.emission, ); }); @@ -141,9 +141,9 @@ export function shouldBehaveLikeNftStake(): void { it("should allow harvesting without withdrawl", async function () { const tokenId = BigNumber.from(1); // Approve nftStake to take the token - await this.mockERC721.connect(this.signers.user1).approve(this.nftStake.address, tokenId); + await this.mockERC721.connect(this.signers.alice).approve(this.nftStake.address, tokenId); // Try to stake it - await expect(this.nftStake.connect(this.signers.user1).stake([tokenId], true)).to.not.be.reverted; + await expect(this.nftStake.connect(this.signers.alice).stake([tokenId], true)).to.not.be.reverted; // Wait 4 blocks await network.provider.send("evm_mine"); @@ -153,17 +153,17 @@ export function shouldBehaveLikeNftStake(): void { // get current earned stake const currentEarnedStake = ( - await this.nftStake.connect(this.signers.user1).rewardOf(tokenId) + await this.nftStake.connect(this.signers.alice).rewardOf(tokenId) ).toNumber(); // get current token balance of user const balanceBeforeHarvest = ( - await this.mockERC20.connect(this.signers.user1).balanceOf(await this.signers.user1.getAddress()) + await this.mockERC20.connect(this.signers.alice).balanceOf(await this.signers.alice.getAddress()) ).toNumber(); // get the staked receipt const stakedAtOriginal = ( - await this.nftStake.connect(this.signers.user1).receipt(tokenId) + await this.nftStake.connect(this.signers.alice).receipt(tokenId) ).from.toNumber(); // get current blockNumer @@ -176,25 +176,25 @@ export function shouldBehaveLikeNftStake(): void { expect(balanceBeforeHarvest).to.eq(0); // should not let you harvest tokens you did not stake - await expect(this.nftStake.connect(this.signers.user2).harvest([tokenId], true)).to.be.revertedWith( + await expect(this.nftStake.connect(this.signers.bob).harvest([tokenId], true)).to.be.revertedWith( "nftstake: not owner", ); - await expect(this.nftStake.connect(this.signers.user2).harvest([tokenId], false)).to.be.revertedWith( + await expect(this.nftStake.connect(this.signers.bob).harvest([tokenId], false)).to.be.revertedWith( "nftstake: must accept terms of service", ); // harvest Stake - await this.nftStake.connect(this.signers.user1).harvest([tokenId], true); + await this.nftStake.connect(this.signers.alice).harvest([tokenId], true); // should have harvested the tokens expect( - (await this.mockERC20.connect(this.signers.user1).balanceOf(await this.signers.user1.getAddress())).toNumber(), + (await this.mockERC20.connect(this.signers.alice).balanceOf(await this.signers.alice.getAddress())).toNumber(), ).to.eq(currentEarnedStake); // check the new receipt const updatedStakeDate = ( - await this.nftStake.connect(this.signers.user1).receipt(tokenId) + await this.nftStake.connect(this.signers.alice).receipt(tokenId) ).from.toNumber(); currentBlock = parseInt(await network.provider.send("eth_blockNumber"), 16); @@ -202,16 +202,16 @@ export function shouldBehaveLikeNftStake(): void { expect(currentBlock).to.eq(updatedStakeDate); // check that there is no pending payout availible - expect((await this.nftStake.connect(this.signers.user1).rewardOf(tokenId)).toNumber()).to.eq(0); + expect((await this.nftStake.connect(this.signers.alice).rewardOf(tokenId)).toNumber()).to.eq(0); // check that nftStake still owns the token - expect(await this.mockERC721.connect(this.signers.user1).ownerOf(tokenId)).to.eq(this.nftStake.address); + expect(await this.mockERC721.connect(this.signers.alice).ownerOf(tokenId)).to.eq(this.nftStake.address); // wait one block await network.provider.send("evm_mine"); // check that there is now a pending payout availible again - expect((await this.nftStake.connect(this.signers.user1).rewardOf(tokenId)).toNumber()).to.eq( + expect((await this.nftStake.connect(this.signers.alice).rewardOf(tokenId)).toNumber()).to.eq( 1 * this.emission, ); }); @@ -219,28 +219,28 @@ export function shouldBehaveLikeNftStake(): void { it("should allow user to unstake if reward gt balance", async function () { const tokenId = BigNumber.from(1); // Approve nftStake to take the token - await this.mockERC721.connect(this.signers.user1).approve(this.nftStake.address, tokenId); + await this.mockERC721.connect(this.signers.alice).approve(this.nftStake.address, tokenId); await this.nftStake.connect(this.signers.dao).setEmissionRate(10000) // Try to stake it - await expect(this.nftStake.connect(this.signers.user1).stake([tokenId], true)).to.not.be.reverted; + await expect(this.nftStake.connect(this.signers.alice).stake([tokenId], true)).to.not.be.reverted; await network.provider.send("evm_mine"); await network.provider.send("evm_mine"); // try to unstake - await expect(this.nftStake.connect(this.signers.user1).unstake([tokenId], true)).to.not.be.reverted; + await expect(this.nftStake.connect(this.signers.alice).unstake([tokenId], true)).to.not.be.reverted; - // confirm user1 owns the token again - expect(await this.mockERC721.connect(this.signers.user1).ownerOf(tokenId)).to.eql( - await this.signers.user1.getAddress(), + // confirm alice owns the token again + expect(await this.mockERC721.connect(this.signers.alice).ownerOf(tokenId)).to.eql( + await this.signers.alice.getAddress(), ); - // check if user1 has been paid the estimated stake + // check if alice has been paid the estimated stake expect( await ( - await this.mockERC20.connect(this.signers.user1).balanceOf(await this.signers.user1.getAddress()) + await this.mockERC20.connect(this.signers.alice).balanceOf(await this.signers.alice.getAddress()) ).toNumber(), ).to.eql(0); }); diff --git a/test/nftStake/NftStake.ts b/test/nftStake/NftStake.ts index 0f0aa1d..7c5d7a6 100644 --- a/test/nftStake/NftStake.ts +++ b/test/nftStake/NftStake.ts @@ -19,12 +19,12 @@ describe("Unit tests", function () { const signers: SignerWithAddress[] = await hre.ethers.getSigners(); this.signers.admin = signers[0]; - this.signers.user1 = signers[1]; - this.signers.user2 = signers[2]; + this.signers.alice = signers[1]; + this.signers.bob = signers[2]; this.signers.dao = signers[3]; }); - describe("NFTStake", function () { + xdescribe("NFTStake", function () { beforeEach(async function () { this.emission = 2; @@ -57,10 +57,10 @@ describe("Unit tests", function () { // Mint some NFTS const adminERC721Instance: MockERC721 = await this.mockERC721.connect(this.signers.admin); - // user1 has tokenId 1 - // user2 has tokenId 2 - await adminERC721Instance.mint(await this.signers.user1.getAddress(), "First"); - await adminERC721Instance.mint(await this.signers.user2.getAddress(), "Second"); + // alice has tokenId 1 + // bob has tokenId 2 + await adminERC721Instance.mint(await this.signers.alice.getAddress(), "First"); + await adminERC721Instance.mint(await this.signers.bob.getAddress(), "Second"); }); shouldBehaveLikeNftStake(); diff --git a/test/types.ts b/test/types.ts index 0ee67a8..797b27f 100644 --- a/test/types.ts +++ b/test/types.ts @@ -1,13 +1,17 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { Fixture } from "ethereum-waffle"; -import { MockERC20, MockERC721, NftStake } from "../typechain"; +import { MockERC20, MockERC721, NftStake, DopeWarsLoot, DopeDAOTest, Receiver, Timelock } from "../typechain"; declare module "mocha" { export interface Context { nftStake: NftStake; mockERC20: MockERC20; mockERC721: MockERC721; emission: number; + loot: DopeWarsLoot; + timelock: Timelock; + dao: DopeDAOTest; + receiver: Receiver; loadFixture: (fixture: Fixture) => Promise; signers: Signers; } @@ -15,7 +19,7 @@ declare module "mocha" { export interface Signers { admin: SignerWithAddress; - user1: SignerWithAddress; - user2: SignerWithAddress; + alice: SignerWithAddress; + bob: SignerWithAddress; dao: SignerWithAddress; } diff --git a/yarn.lock b/yarn.lock index 79c6011..a915fda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -274,7 +274,7 @@ __metadata: "@nomiclabs/hardhat-ethers": ^2.0.2 "@nomiclabs/hardhat-etherscan": ^2.1.6 "@nomiclabs/hardhat-waffle": ^2.0.1 - "@openzeppelin/contracts": ">=4.3.1" + "@openzeppelin/contracts": ^4.3.2 "@typechain/ethers-v5": ^7.0.1 "@typechain/hardhat": ^2.3.0 "@types/chai": ^4.2.21 @@ -1039,10 +1039,10 @@ __metadata: languageName: node linkType: hard -"@openzeppelin/contracts@npm:>=4.3.1": - version: 4.3.1 - resolution: "@openzeppelin/contracts@npm:4.3.1" - checksum: ddfa5bb65f5e645a618e0062c83fa6a44499cece47ef5dd62cc51c0da5f6c3253c9def9fb6c0086cc37a463fb2f8b2d7dc271ab78d876d99134b57baba2e8639 +"@openzeppelin/contracts@npm:^4.3.2": + version: 4.3.2 + resolution: "@openzeppelin/contracts@npm:4.3.2" + checksum: 9032453eef3e10b6453b667c7edd3381292665083b855ca8a8b5f64b473e768eacb53516865fb3ce1e493deadbb977069e483cce5650c42807a742ab59a18695 languageName: node linkType: hard