Skip to content

Commit

Permalink
test: add unit tests and minor fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
PacificYield committed Dec 19, 2024
1 parent 481f54f commit e536334
Show file tree
Hide file tree
Showing 8 changed files with 453 additions and 15 deletions.
10 changes: 3 additions & 7 deletions contracts/finance/ConfidentialVestingWallet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import "fhevm/lib/TFHE.sol";

import { IConfidentialERC20 } from "../token/ERC20/IConfidentialERC20.sol";

import "hardhat/console.sol";

/**
* @title ConfidentialVestingWallet
* @notice This contract offers a simple vesting wallet for ConfidentialERC20 tokens.
Expand Down Expand Up @@ -36,9 +34,8 @@ abstract contract ConfidentialVestingWallet {

/// @notice Constant for zero using TFHE.
/// @dev Since it is expensive to compute 0, it is stored instead.
/// However, is not possible to define it as constant due to TFHE constraints.
/* solhint-disable var-name-mixedcase*/
euint64 internal _EUINT64_ZERO;
euint64 internal immutable _EUINT64_ZERO;

/// @notice Total encrypted amount released (to the beneficiary).
euint64 internal _amountReleased;
Expand All @@ -56,7 +53,7 @@ abstract contract ConfidentialVestingWallet {
END_TIMESTAMP = startTimestamp_ + duration_;
BENEFICIARY = beneficiary_;

/// @dev Store this constant-like variable in the storage.
/// @dev Store this constant variable in the storage.
_EUINT64_ZERO = TFHE.asEuint64(0);
_amountReleased = _EUINT64_ZERO;

Expand All @@ -70,12 +67,11 @@ abstract contract ConfidentialVestingWallet {
*/
function release() public virtual {
euint64 amount = _releasable();

euint64 amountReleased = TFHE.add(_amountReleased, amount);
_amountReleased = amountReleased;

TFHE.allow(amountReleased, BENEFICIARY);
TFHE.allowThis(amountReleased);

TFHE.allowTransient(amount, address(CONFIDENTIAL_ERC20));
CONFIDENTIAL_ERC20.transfer(BENEFICIARY, amount);

Expand Down
15 changes: 7 additions & 8 deletions contracts/finance/ConfidentialVestingWalletCliff.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,32 @@ import { ConfidentialVestingWallet } from "./ConfidentialVestingWallet.sol";
* @dev This implementation is a linear vesting curve with a cliff.
* To use with the native asset, it is necessary to wrap the native asset to a ConfidentialERC20-like token.
*/

abstract contract VestingWalletCliff is ConfidentialVestingWallet {
abstract contract ConfidentialVestingWalletCliff is ConfidentialVestingWallet {
/// @notice Returned if the cliff duration is greater than the vesting duration.
error InvalidCliffDuration(uint64 cliffSeconds, uint64 durationSeconds);

/// @notice Cliff duration (in seconds).
/// @notice Cliff timestamp.
uint64 public immutable CLIFF;

/**
* @param beneficiary_ Beneficiary address.
* @param token_ Confidential token address.
* @param startTimestamp_ Start timestamp.
* @param duration_ Duration (in seconds).
* @param cliff_ Cliff (in seconds).
* @param cliffSeconds_ Cliff (in seconds).
*/
constructor(
address beneficiary_,
address token_,
uint64 startTimestamp_,
uint64 duration_,
uint64 cliff_
uint64 cliffSeconds_
) ConfidentialVestingWallet(beneficiary_, token_, startTimestamp_, duration_) {
if (cliff_ > duration_) {
revert InvalidCliffDuration(cliff_, duration_);
if (cliffSeconds_ > duration_) {
revert InvalidCliffDuration(cliffSeconds_, duration_);
}

CLIFF = startTimestamp_ + cliff_;
CLIFF = startTimestamp_ + cliffSeconds_;
}

/**
Expand Down
16 changes: 16 additions & 0 deletions contracts/test/finance/TestConfidentialVestingWallet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.24;

import { ConfidentialVestingWallet } from "../../finance/ConfidentialVestingWallet.sol";
import { SepoliaZamaFHEVMConfig } from "fhevm/config/ZamaFHEVMConfig.sol";

contract TestConfidentialVestingWallet is SepoliaZamaFHEVMConfig, ConfidentialVestingWallet {
constructor(
address beneficiary_,
address token_,
uint64 startTimestamp_,
uint64 duration_
) ConfidentialVestingWallet(beneficiary_, token_, startTimestamp_, duration_) {
//
}
}
17 changes: 17 additions & 0 deletions contracts/test/finance/TestConfidentialVestingWalletCliff.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.24;

import { ConfidentialVestingWalletCliff } from "../../finance/ConfidentialVestingWalletCliff.sol";
import { SepoliaZamaFHEVMConfig } from "fhevm/config/ZamaFHEVMConfig.sol";

contract TestConfidentialVestingWalletCliff is SepoliaZamaFHEVMConfig, ConfidentialVestingWalletCliff {
constructor(
address beneficiary_,
address token_,
uint64 startTimestamp_,
uint64 duration_,
uint64 cliff_
) ConfidentialVestingWalletCliff(beneficiary_, token_, startTimestamp_, duration_, cliff_) {
//
}
}
30 changes: 30 additions & 0 deletions test/finance/ConfidentialVestingWallet.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Signer } from "ethers";
import { FhevmInstance } from "fhevmjs/node";
import { ethers } from "hardhat";

import type { ConfidentialVestingWallet, TestConfidentialVestingWallet } from "../../types";
import { reencryptEuint64 } from "../reencrypt";

export async function deployConfidentialVestingWalletFixture(
account: Signer,
beneficiaryAddress: string,
token: string,
startTimestamp: bigint,
duration: bigint,
): Promise<TestConfidentialVestingWallet> {
const contractFactory = await ethers.getContractFactory("TestConfidentialVestingWallet");
const contract = await contractFactory.connect(account).deploy(beneficiaryAddress, token, startTimestamp, duration);
await contract.waitForDeployment();
return contract;
}

export async function reencryptReleased(
account: Signer,
instance: FhevmInstance,
vestingWallet: ConfidentialVestingWallet,
vestingWalletAddress: string,
): Promise<bigint> {
const releasedHandled = await vestingWallet.released();
const releasedAmount = await reencryptEuint64(account, instance, releasedHandled, vestingWalletAddress);
return releasedAmount;
}
158 changes: 158 additions & 0 deletions test/finance/ConfidentialVestingWallet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { expect } from "chai";
import { parseUnits } from "ethers";
import { ethers } from "hardhat";

import { deployConfidentialERC20Fixture, reencryptBalance } from "../confidentialERC20/ConfidentialERC20.fixture";
import { createInstance } from "../instance";
import { getSigners, initSigners } from "../signers";
import { deployConfidentialVestingWalletFixture, reencryptReleased } from "./ConfidentialVestingWallet.fixture";

describe("ConfidentialVestingWallet", function () {
before(async function () {
await initSigners();
this.signers = await getSigners();
this.instance = await createInstance();
});

beforeEach(async function () {
const latestBlockNumber = await ethers.provider.getBlockNumber();
const block = await ethers.provider.getBlock(latestBlockNumber);

this.beneficiary = this.signers.bob;
this.beneficiaryAddress = this.signers.bob.address;

const contractConfidentialERC20 = await deployConfidentialERC20Fixture(
this.signers.alice,
"Naraggara",
"NARA",
this.signers.alice.address,
);
this.confidentialERC20Address = await contractConfidentialERC20.getAddress();
this.confidentialERC20 = contractConfidentialERC20;
this.startTimestamp = BigInt(block!.timestamp + 3600);
this.duration = BigInt(36_000); // 36,000 seconds

const contractConfidentialVestingWallet = await deployConfidentialVestingWalletFixture(
this.signers.alice,
this.beneficiaryAddress,
this.confidentialERC20Address,
this.startTimestamp,
this.duration,
);

this.confidentialVestingWallet = contractConfidentialVestingWallet;
this.confidentialVestingWalletAddress = await contractConfidentialVestingWallet.getAddress();
});

it("post-deployment state", async function () {
expect(await this.confidentialVestingWallet.BENEFICIARY()).to.equal(this.beneficiaryAddress);
expect(await this.confidentialVestingWallet.CONFIDENTIAL_ERC20()).to.equal(this.confidentialERC20);
expect(await this.confidentialVestingWallet.DURATION()).to.equal(this.duration);
expect(await this.confidentialVestingWallet.END_TIMESTAMP()).to.be.eq(this.startTimestamp + this.duration);
expect(await this.confidentialVestingWallet.START_TIMESTAMP()).to.be.eq(this.startTimestamp);
expect(
await reencryptReleased(
this.beneficiary,
this.instance,
this.confidentialVestingWallet,
this.confidentialVestingWalletAddress,
),
).to.be.eq(0n);
});

it("can release", async function () {
// 10M
const amount = parseUnits("10000000", 6);

let tx = await this.confidentialERC20.connect(this.signers.alice).mint(this.signers.alice, amount);
await tx.wait();

const input = this.instance.createEncryptedInput(this.confidentialERC20Address, this.signers.alice.address);
input.add64(amount);
const encryptedTransferAmount = await input.encrypt();

tx = await this.confidentialERC20
.connect(this.signers.alice)
[
"transfer(address,bytes32,bytes)"
](this.confidentialVestingWalletAddress, encryptedTransferAmount.handles[0], encryptedTransferAmount.inputProof);

await tx.wait();

let nextTimestamp = this.startTimestamp;
await ethers.provider.send("evm_setNextBlockTimestamp", [nextTimestamp.toString()]);

tx = await this.confidentialVestingWallet.connect(this.beneficiary).release();
await expect(tx).to.emit(this.confidentialVestingWallet, "ConfidentialERC20Released");

// It should be equal to 0 because the vesting has not started.
expect(
await reencryptReleased(
this.beneficiary,
this.instance,
this.confidentialVestingWallet,
this.confidentialVestingWalletAddress,
),
).to.be.eq(0n);

nextTimestamp = this.startTimestamp + this.duration / BigInt(4);
await ethers.provider.send("evm_setNextBlockTimestamp", [nextTimestamp.toString()]);

tx = await this.confidentialVestingWallet.connect(this.beneficiary).release();
await tx.wait();

// It should be equal to 1/4 of the amount vested.
expect(
await reencryptReleased(
this.beneficiary,
this.instance,
this.confidentialVestingWallet,
this.confidentialVestingWalletAddress,
),
).to.be.eq(BigInt(amount) / BigInt(4));

expect(
await reencryptBalance(this.beneficiary, this.instance, this.confidentialERC20, this.confidentialERC20Address),
).to.be.eq(BigInt(amount) / BigInt(4));

nextTimestamp = this.startTimestamp + this.duration / BigInt(2);
await ethers.provider.send("evm_setNextBlockTimestamp", [nextTimestamp.toString()]);

tx = await this.confidentialVestingWallet.connect(this.beneficiary).release();
await tx.wait();

// It should be equal to 1/4 of the amount vested since 1/4 was already collected.
expect(
await reencryptReleased(
this.beneficiary,
this.instance,
this.confidentialVestingWallet,
this.confidentialVestingWalletAddress,
),
).to.be.eq(BigInt(amount) / BigInt(2));

expect(
await reencryptBalance(this.beneficiary, this.instance, this.confidentialERC20, this.confidentialERC20Address),
).to.be.eq(BigInt(amount) / BigInt(2));

nextTimestamp = this.startTimestamp + this.duration;
await ethers.provider.send("evm_setNextBlockTimestamp", [nextTimestamp.toString()]);

tx = await this.confidentialVestingWallet.connect(this.beneficiary).release();
await tx.wait();

// It should be equal to 1/2 of the amount vested since 2/4 was already collected.
expect(
await reencryptReleased(
this.beneficiary,
this.instance,
this.confidentialVestingWallet,
this.confidentialVestingWalletAddress,
),
).to.be.eq(BigInt(amount));

expect(
await reencryptBalance(this.beneficiary, this.instance, this.confidentialERC20, this.confidentialERC20Address),
).to.be.eq(BigInt(amount));
});
});
20 changes: 20 additions & 0 deletions test/finance/ConfidentialVestingWalletCliff.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Signer } from "ethers";
import { ethers } from "hardhat";

import type { TestConfidentialVestingWalletCliff } from "../../types";

export async function deployConfidentialVestingWalletCliffFixture(
account: Signer,
beneficiaryAddress: string,
token: string,
startTimestamp: bigint,
duration: bigint,
cliffSeconds: bigint,
): Promise<TestConfidentialVestingWalletCliff> {
const contractFactory = await ethers.getContractFactory("TestConfidentialVestingWalletCliff");
const contract = await contractFactory
.connect(account)
.deploy(beneficiaryAddress, token, startTimestamp, duration, cliffSeconds);
await contract.waitForDeployment();
return contract;
}
Loading

0 comments on commit e536334

Please sign in to comment.