From f43ec3c5668157c787c3f5caa7d18b2c92faaddb Mon Sep 17 00:00:00 2001 From: Isaac Frank Date: Sun, 31 Dec 2023 10:18:48 +0100 Subject: [PATCH] refactor: replace greeter with lock contract (#182) * refactor: replace greeter with lock contract * test: reorder test * fix: fix type error docs: improve parameter wording --------- Co-authored-by: Paul Razvan Berg --- contracts/Greeter.sol | 28 --------- contracts/Lock.sol | 36 +++++++++++ deploy/deploy.ts | 16 +++-- hardhat.config.ts | 3 +- package.json | 4 +- tasks/greet.ts | 19 ------ tasks/lock.ts | 63 +++++++++++++++++++ tasks/taskDeploy.ts | 12 ---- test/greeter/Greeter.behavior.ts | 10 --- test/greeter/Greeter.fixture.ts | 16 ----- test/greeter/Greeter.ts | 26 -------- test/lock/Lock.fixture.ts | 22 +++++++ test/lock/Lock.ts | 102 +++++++++++++++++++++++++++++++ test/types.ts | 4 +- 14 files changed, 239 insertions(+), 122 deletions(-) delete mode 100644 contracts/Greeter.sol create mode 100644 contracts/Lock.sol delete mode 100644 tasks/greet.ts create mode 100644 tasks/lock.ts delete mode 100644 tasks/taskDeploy.ts delete mode 100644 test/greeter/Greeter.behavior.ts delete mode 100644 test/greeter/Greeter.fixture.ts delete mode 100644 test/greeter/Greeter.ts create mode 100644 test/lock/Lock.fixture.ts create mode 100644 test/lock/Lock.ts diff --git a/contracts/Greeter.sol b/contracts/Greeter.sol deleted file mode 100644 index af930363..00000000 --- a/contracts/Greeter.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.4; - -import { console } from "hardhat/console.sol"; - -error GreeterError(); - -contract Greeter { - string public greeting; - - constructor(string memory _greeting) { - console.log("Deploying a Greeter with greeting:", _greeting); - greeting = _greeting; - } - - function greet() public view returns (string memory) { - return greeting; - } - - function setGreeting(string memory _greeting) public { - console.log("Changing greeting from '%s' to '%s'", greeting, _greeting); - greeting = _greeting; - } - - function throwError() external pure { - revert GreeterError(); - } -} diff --git a/contracts/Lock.sol b/contracts/Lock.sol new file mode 100644 index 00000000..c236c27b --- /dev/null +++ b/contracts/Lock.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.9; + +error InvalidUnlockTime(uint256 unlockTime); +error NotOwner(address owner); +error UnlockTimeNotReached(uint256 unlockTime); + +contract Lock { + uint256 public unlockTime; + address payable public owner; + + event Withdrawal(uint256 amount, uint256 when); + + constructor(uint256 _unlockTime) payable { + if (block.timestamp >= _unlockTime) { + revert InvalidUnlockTime(_unlockTime); + } + + unlockTime = _unlockTime; + owner = payable(msg.sender); + } + + function withdraw() public { + if (block.timestamp < unlockTime) { + revert UnlockTimeNotReached(unlockTime); + } + + if (msg.sender != owner) { + revert NotOwner(owner); + } + + emit Withdrawal(address(this).balance, block.timestamp); + + owner.transfer(address(this).balance); + } +} diff --git a/deploy/deploy.ts b/deploy/deploy.ts index 30e01abb..f09cf438 100644 --- a/deploy/deploy.ts +++ b/deploy/deploy.ts @@ -1,18 +1,24 @@ import { DeployFunction } from "hardhat-deploy/types"; import { HardhatRuntimeEnvironment } from "hardhat/types"; +const DAY_IN_SECONDS = 60 * 60 * 24; +const NOW_IN_SECONDS = Math.round(Date.now() / 1000); +const UNLOCK_IN_X_DAYS = NOW_IN_SECONDS + DAY_IN_SECONDS * 1; // 1 DAY + const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { deployer } = await hre.getNamedAccounts(); const { deploy } = hre.deployments; + const lockedAmount = hre.ethers.parseEther("0.01").toString(); - const greeter = await deploy("Greeter", { + const lock = await deploy("Lock", { from: deployer, - args: ["Bonjour, le monde!"], + args: [UNLOCK_IN_X_DAYS], log: true, + value: lockedAmount, }); - console.log(`Greeter contract: `, greeter.address); + console.log(`Lock contract: `, lock.address); }; export default func; -func.id = "deploy_greeter"; // id required to prevent reexecution -func.tags = ["Greeter"]; +func.id = "deploy_lock"; // id required to prevent reexecution +func.tags = ["Lock"]; diff --git a/hardhat.config.ts b/hardhat.config.ts index 12f70bbb..1e910162 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -5,8 +5,7 @@ import { vars } from "hardhat/config"; import type { NetworkUserConfig } from "hardhat/types"; import "./tasks/accounts"; -import "./tasks/greet"; -import "./tasks/taskDeploy"; +import "./tasks/lock"; // Run 'npx hardhat vars setup' to see the list of variables that need to be set diff --git a/package.json b/package.json index 113f2713..dd24f3c1 100644 --- a/package.json +++ b/package.json @@ -71,8 +71,8 @@ "postcompile": "pnpm typechain", "prettier:check": "prettier --check \"**/*.{js,json,md,sol,ts,yml}\"", "prettier:write": "prettier --write \"**/*.{js,json,md,sol,ts,yml}\"", - "task:deployGreeter": "hardhat task:deployGreeter", - "task:setGreeting": "hardhat task:setGreeting", + "task:deployLock": "hardhat task:deployLock", + "task:withdraw": "hardhat task:withdraw", "test": "hardhat test", "typechain": "cross-env TS_NODE_TRANSPILE_ONLY=true hardhat typechain" } diff --git a/tasks/greet.ts b/tasks/greet.ts deleted file mode 100644 index 7e69d0b8..00000000 --- a/tasks/greet.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { task } from "hardhat/config"; -import type { TaskArguments } from "hardhat/types"; - -task("task:setGreeting") - .addParam("greeting", "Say hello, be nice") - .addParam("account", "Specify which account [0, 9]") - .setAction(async function (taskArguments: TaskArguments, hre) { - const { ethers, deployments } = hre; - - const Greeter = await deployments.get("Greeter"); - - const signers = await ethers.getSigners(); - - const greeter = await ethers.getContractAt("Greeter", Greeter.address); - - await greeter.connect(signers[taskArguments.account]).setGreeting(taskArguments.greeting); - - console.log("Greeting set: ", taskArguments.greeting); - }); diff --git a/tasks/lock.ts b/tasks/lock.ts new file mode 100644 index 00000000..47aa8c2a --- /dev/null +++ b/tasks/lock.ts @@ -0,0 +1,63 @@ +import { task } from "hardhat/config"; +import type { TaskArguments } from "hardhat/types"; + +function distance(past: number, future: number): string { + // get total seconds between the times + let delta = future - past; + + // calculate (and subtract) whole days + const days = Math.floor(delta / 86400); + delta -= days * 86400; + + // calculate (and subtract) whole hours + const hours = Math.floor(delta / 3600) % 24; + delta -= hours * 3600; + + // calculate (and subtract) whole minutes + const minutes = Math.floor(delta / 60) % 60; + delta -= minutes * 60; + + // what's left is seconds + const seconds = delta % 60; // in theory the modulus is not required + + return `${days} day(s), ${hours} hour(s), ${minutes} minute(s) and ${seconds} second(s)`; +} + +task("task:withdraw", "Calls the withdraw function of Lock Contract") + .addOptionalParam("address", "Optionally specify the Lock address to withdraw") + .addParam("account", "Specify which account [0, 9]") + .setAction(async function (taskArguments: TaskArguments, hre) { + const { ethers, deployments } = hre; + + const Lock = taskArguments.address ? { address: taskArguments.address } : await deployments.get("Lock"); + + const signers = await ethers.getSigners(); + console.log(taskArguments.address); + + const lock = await ethers.getContractAt("Lock", Lock.address); + + const initialBalance = await ethers.provider.getBalance(Lock.address); + await lock.connect(signers[taskArguments.account]).withdraw(); + const finalBalance = await ethers.provider.getBalance(Lock.address); + + console.log("Contract balance before withdraw", ethers.formatEther(initialBalance)); + console.log("Contract balance after withdraw", ethers.formatEther(finalBalance)); + + console.log("Lock Withdraw Success"); + }); + +task("task:deployLock", "Deploys Lock Contract") + .addParam("unlock", "When to unlock funds in seconds (number of seconds into the futrue)") + .addParam("value", "How much ether you intend locking (in ether not wei, e.g., 0.1)") + .setAction(async function (taskArguments: TaskArguments, { ethers }) { + const NOW_IN_SECONDS = Math.round(Date.now() / 1000); + + const signers = await ethers.getSigners(); + const lockedAmount = ethers.parseEther(taskArguments.value); + const unlockTime = NOW_IN_SECONDS + parseInt(taskArguments.unlock); + const lockFactory = await ethers.getContractFactory("Lock"); + console.log(`Deploying Lock and locking ${taskArguments.value} ETH for ${distance(NOW_IN_SECONDS, unlockTime)}`); + const lock = await lockFactory.connect(signers[0]).deploy(unlockTime, { value: lockedAmount }); + await lock.waitForDeployment(); + console.log("Lock deployed to: ", await lock.getAddress()); + }); diff --git a/tasks/taskDeploy.ts b/tasks/taskDeploy.ts deleted file mode 100644 index 13f4ee37..00000000 --- a/tasks/taskDeploy.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { task } from "hardhat/config"; -import type { TaskArguments } from "hardhat/types"; - -task("task:deployGreeter") - .addParam("greeting", "Say hello, be nice") - .setAction(async function (taskArguments: TaskArguments, { ethers }) { - const signers = await ethers.getSigners(); - const greeterFactory = await ethers.getContractFactory("Greeter"); - const greeter = await greeterFactory.connect(signers[0]).deploy(taskArguments.greeting); - await greeter.waitForDeployment(); - console.log("Greeter deployed to: ", await greeter.getAddress()); - }); diff --git a/test/greeter/Greeter.behavior.ts b/test/greeter/Greeter.behavior.ts deleted file mode 100644 index cd51397a..00000000 --- a/test/greeter/Greeter.behavior.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { expect } from "chai"; - -export function shouldBehaveLikeGreeter(): void { - it("should return the new greeting once it's changed", async function () { - expect(await this.greeter.connect(this.signers.admin).greet()).to.equal("Hello, world!"); - - await this.greeter.setGreeting("Bonjour, le monde!"); - expect(await this.greeter.connect(this.signers.admin).greet()).to.equal("Bonjour, le monde!"); - }); -} diff --git a/test/greeter/Greeter.fixture.ts b/test/greeter/Greeter.fixture.ts deleted file mode 100644 index 6636c07c..00000000 --- a/test/greeter/Greeter.fixture.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ethers } from "hardhat"; - -import type { Greeter } from "../../types/Greeter"; -import type { Greeter__factory } from "../../types/factories/Greeter__factory"; - -export async function deployGreeterFixture(): Promise<{ greeter: Greeter }> { - const signers = await ethers.getSigners(); - const admin = signers[0]; - - const greeting = "Hello, world!"; - const greeterFactory = await ethers.getContractFactory("Greeter"); - const greeter = await greeterFactory.connect(admin).deploy(greeting); - await greeter.waitForDeployment(); - - return { greeter }; -} diff --git a/test/greeter/Greeter.ts b/test/greeter/Greeter.ts deleted file mode 100644 index 7f531db5..00000000 --- a/test/greeter/Greeter.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; -import { ethers } from "hardhat"; - -import type { Signers } from "../types"; -import { shouldBehaveLikeGreeter } from "./Greeter.behavior"; -import { deployGreeterFixture } from "./Greeter.fixture"; - -describe("Unit tests", function () { - before(async function () { - this.signers = {} as Signers; - - const signers = await ethers.getSigners(); - this.signers.admin = signers[0]; - - this.loadFixture = loadFixture; - }); - - describe("Greeter", function () { - beforeEach(async function () { - const { greeter } = await this.loadFixture(deployGreeterFixture); - this.greeter = greeter; - }); - - shouldBehaveLikeGreeter(); - }); -}); diff --git a/test/lock/Lock.fixture.ts b/test/lock/Lock.fixture.ts new file mode 100644 index 00000000..4ffe253e --- /dev/null +++ b/test/lock/Lock.fixture.ts @@ -0,0 +1,22 @@ +import { time } from "@nomicfoundation/hardhat-network-helpers"; +import { ethers } from "hardhat"; + +import type { Lock } from "../../types/Lock"; +import type { Lock__factory } from "../../types/factories/Lock__factory"; + +export async function deployLockFixture() { + const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60; + const ONE_GWEI = 1_000_000_000; + + const lockedAmount = ONE_GWEI; + const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS; + + // Contracts are deployed using the first signer/account by default + const [owner, otherAccount] = await ethers.getSigners(); + + const Lock = (await ethers.getContractFactory("Lock")) as Lock__factory; + const lock = (await Lock.deploy(unlockTime, { value: lockedAmount })) as Lock; + const lock_address = await lock.getAddress(); + + return { lock, lock_address, unlockTime, lockedAmount, owner, otherAccount }; +} diff --git a/test/lock/Lock.ts b/test/lock/Lock.ts new file mode 100644 index 00000000..932df747 --- /dev/null +++ b/test/lock/Lock.ts @@ -0,0 +1,102 @@ +import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; +import { loadFixture, time } from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import type { Signers } from "../types"; +import { deployLockFixture } from "./Lock.fixture"; + +describe("Lock", function () { + before(async function () { + this.signers = {} as Signers; + + const signers = await ethers.getSigners(); + this.signers.admin = signers[0]; + + this.loadFixture = loadFixture; + }); + + describe("Deployment", function () { + beforeEach(async function () { + const { lock, lock_address, unlockTime, owner, lockedAmount } = await this.loadFixture(deployLockFixture); + this.lock = lock; + this.lock_address = lock_address; + this.unlockTime = unlockTime; + this.owner = owner; + this.lockedAmount = lockedAmount; + }); + + it("Should fail if the unlockTime is not in the future", async function () { + // We don't use the fixture here because we want a different deployment + const latestTime = await time.latest(); + const Lock = await ethers.getContractFactory("Lock"); + await expect(Lock.deploy(latestTime, { value: 1 })).to.be.revertedWithCustomError(Lock, "InvalidUnlockTime"); + }); + + it("Should set the right unlockTime", async function () { + expect(await this.lock.unlockTime()).to.equal(this.unlockTime); + }); + + it("Should set the right owner", async function () { + expect(await this.lock.owner()).to.equal(this.owner.address); + }); + + it("Should receive and store the funds to lock", async function () { + expect(await ethers.provider.getBalance(this.lock_address)).to.equal(this.lockedAmount); + }); + }); + + describe("Withdrawals", function () { + beforeEach(async function () { + const { lock, unlockTime, owner, lockedAmount, otherAccount } = await this.loadFixture(deployLockFixture); + this.lock = lock; + this.unlockTime = unlockTime; + this.owner = owner; + this.lockedAmount = lockedAmount; + this.otherAccount = otherAccount; + }); + + describe("Validations", function () { + it("Should revert with the right error if called too soon", async function () { + await expect(this.lock.withdraw()).to.be.revertedWithCustomError(this.lock, "UnlockTimeNotReached"); + }); + + it("Should revert with the right error if called from another account", async function () { + // We can increase the time in Hardhat Network + await time.increaseTo(this.unlockTime); + + // We use lock.connect() to send a transaction from another account + await expect(this.lock.connect(this.otherAccount).withdraw()).to.be.revertedWithCustomError( + this.lock, + "NotOwner", + ); + }); + + it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () { + // Transactions are sent using the first signer by default + await time.increaseTo(this.unlockTime); + + await expect(this.lock.withdraw()).not.to.be.reverted; + }); + }); + + describe("Events", function () { + it("Should emit an event on withdrawals", async function () { + await time.increaseTo(this.unlockTime); + + await expect(this.lock.withdraw()).to.emit(this.lock, "Withdrawal").withArgs(this.lockedAmount, anyValue); // We accept any value as `when` arg + }); + }); + + describe("Transfers", function () { + it("Should transfer the funds to the owner", async function () { + await time.increaseTo(this.unlockTime); + + await expect(this.lock.withdraw()).to.changeEtherBalances( + [this.owner, this.lock], + [this.lockedAmount, -this.lockedAmount], + ); + }); + }); + }); +}); diff --git a/test/types.ts b/test/types.ts index 3e7cb89f..645314ee 100644 --- a/test/types.ts +++ b/test/types.ts @@ -1,12 +1,12 @@ import type { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/dist/src/signer-with-address"; -import type { Greeter } from "../types/Greeter"; +import type { Lock } from "../types/Lock"; type Fixture = () => Promise; declare module "mocha" { export interface Context { - greeter: Greeter; + lock: Lock; loadFixture: (fixture: Fixture) => Promise; signers: Signers; }