Skip to content

Commit

Permalink
refactor: replace greeter with lock contract (#182)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
zikyfranky and PaulRBerg committed Dec 31, 2023
1 parent e7224e5 commit f43ec3c
Show file tree
Hide file tree
Showing 14 changed files with 239 additions and 122 deletions.
28 changes: 0 additions & 28 deletions contracts/Greeter.sol

This file was deleted.

36 changes: 36 additions & 0 deletions contracts/Lock.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
16 changes: 11 additions & 5 deletions deploy/deploy.ts
Original file line number Diff line number Diff line change
@@ -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"];
3 changes: 1 addition & 2 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
19 changes: 0 additions & 19 deletions tasks/greet.ts

This file was deleted.

63 changes: 63 additions & 0 deletions tasks/lock.ts
Original file line number Diff line number Diff line change
@@ -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());
});
12 changes: 0 additions & 12 deletions tasks/taskDeploy.ts

This file was deleted.

10 changes: 0 additions & 10 deletions test/greeter/Greeter.behavior.ts

This file was deleted.

16 changes: 0 additions & 16 deletions test/greeter/Greeter.fixture.ts

This file was deleted.

26 changes: 0 additions & 26 deletions test/greeter/Greeter.ts

This file was deleted.

22 changes: 22 additions & 0 deletions test/lock/Lock.fixture.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 102 additions & 0 deletions test/lock/Lock.ts
Original file line number Diff line number Diff line change
@@ -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],
);
});
});
});
});
Loading

0 comments on commit f43ec3c

Please sign in to comment.