Jelajahi Sumber

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 <paul.razvan.berg@gmail.com>
Isaac Frank 1 tahun lalu
induk
melakukan
f43ec3c566

+ 0 - 28
contracts/Greeter.sol

@@ -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();
-    }
-}

+ 36 - 0
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);
+    }
+}

+ 11 - 5
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"];

+ 1 - 2
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
 

+ 2 - 2
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"
   }

+ 0 - 19
tasks/greet.ts

@@ -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);
-  });

+ 63 - 0
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());
+  });

+ 0 - 12
tasks/taskDeploy.ts

@@ -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());
-  });

+ 0 - 10
test/greeter/Greeter.behavior.ts

@@ -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!");
-  });
-}

+ 0 - 16
test/greeter/Greeter.fixture.ts

@@ -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 };
-}

+ 0 - 26
test/greeter/Greeter.ts

@@ -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();
-  });
-});

+ 22 - 0
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 };
+}

+ 102 - 0
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],
+        );
+      });
+    });
+  });
+});

+ 2 - 2
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<T> = () => Promise<T>;
 
 declare module "mocha" {
   export interface Context {
-    greeter: Greeter;
+    lock: Lock;
     loadFixture: <T>(fixture: Fixture<T>) => Promise<T>;
     signers: Signers;
   }