Skip to content

Commit

Permalink
test: improve existing tests and add new ones (#10)
Browse files Browse the repository at this point in the history
* test: create tests draft for `YieldStreamerPrimary` contract
* test: fix test failures
* test: reorganize and improve existing tests
* style: fix formatting issues in the Hardhat config file
* test: improve tests and add new ones
* test: fix a strange Codacy issue
* test: fix a strange Codacy issue once again
---------
Co-authored-by: Igor Senych <[email protected]>
  • Loading branch information
EvgeniiZaitsevCW authored Mar 7, 2025
1 parent bc563f0 commit d10fd31
Show file tree
Hide file tree
Showing 11 changed files with 3,307 additions and 1,420 deletions.
72 changes: 72 additions & 0 deletions contracts/mocks/YieldStreamerV1Mock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import { IYieldStreamerV1 } from "../interfaces/IYieldStreamerV1.sol";

/**
* @title YieldStreamerV1Mock contract
* @author CloudWalk Inc. (See https://www.cloudwalk.io)
* @dev An implementation of the {YieldStreamerV1} contract for testing purposes.
*/
contract YieldStreamerV1Mock is IYieldStreamerV1 {
/**
* @dev Emitted when an blocklist function is called.
*/
event YieldStreamerV1Mock_BlocklistCalled(address account);

// ------------------ Storage---- ----------------------------- //

mapping(address => ClaimResult) private _claimAllPreview;
mapping(address => bool) private _isBlocklister;

// ------------------ IYieldStreamerV1 ------------------------ //

/**
* @inheritdoc IYieldStreamerV1
*/
function claimAllPreview(address account) external view returns (ClaimResult memory) {
return _claimAllPreview[account];
}

/**
* @inheritdoc IYieldStreamerV1
*/
function blocklist(address account) external {
emit YieldStreamerV1Mock_BlocklistCalled(account);
}

/**
* @inheritdoc IYieldStreamerV1
*/
function isBlocklister(address account) external view returns (bool) {
return _isBlocklister[account];
}

/**
* @inheritdoc IYieldStreamerV1
*/
function getAccountGroup(address account) external view returns (bytes32) {
return bytes32(0);
}

// ------------------ Functions ------------------------------- //

/**
* @dev Sets the preview result for a given account.
* @param account The address of the account to set the preview for.
* @param preview The preview result to set for the account.
*/
function setClaimAllPreview(address account, ClaimResult memory preview) external {
_claimAllPreview[account] = preview;
}

/**
* @dev Sets the blocklister status for a given account.
* @param account The address of the account to set the blocklister status for.
* @param isBlocklister_ The blocklister status to set for the account.
*/
function setBlocklister(address account, bool isBlocklister_) external {
_isBlocklister[account] = isBlocklister_;
}
}
31 changes: 17 additions & 14 deletions contracts/mocks/tokens/ERC20TokenMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,29 +31,17 @@ contract ERC20TokenMock is ERC20 {
* @param account The address of an account to mint for.
* @param amount The amount of tokens to mint.
*/
function mint(address account, uint256 amount) external returns (bool) {
function mint(address account, uint256 amount) external {
_mint(account, amount);

if (_hook != address(0)) {
IERC20Hook(_hook).afterTokenTransfer(address(0), account, amount);
}

return true;
}

/**
* @dev Calls the appropriate internal function to burn needed amount of tokens for an account.
* @param account The address of an account to burn for.
* @param amount The amount of tokens to burn.
*/
function burn(address account, uint256 amount) external returns (bool) {
function burn(address account, uint256 amount) external {
_burn(account, amount);

if (_hook != address(0)) {
IERC20Hook(_hook).afterTokenTransfer(account, address(0), amount);
}

return true;
}

/**
Expand All @@ -64,4 +52,19 @@ contract ERC20TokenMock is ERC20 {
_hook = hook;
return true;
}

// ------------------ Internal functions ---------------------- //

/**
* @dev Overrides the default implementation of the {ERC20} contract to call the hook after the transfer.
* @param from The address of the sender.
* @param to The address of the recipient.
* @param value The amount of tokens to transfer.
*/
function _update(address from, address to, uint256 value) internal virtual override {
super._update(from, to, value);
if (_hook != address(0)) {
IERC20Hook(_hook).afterTokenTransfer(from, to, value);
}
}
}
46 changes: 44 additions & 2 deletions contracts/testable/YieldStreamerTestable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,42 @@
pragma solidity 0.8.24;

import { YieldStreamer } from "../YieldStreamer.sol";
import { Bitwise } from "../libs/Bitwise.sol";

/**
* @title YieldStreamerTestable contract
* @author CloudWalk Inc. (See https://www.cloudwalk.io)
* @dev Implements additional functions to test private and internal functions of base contracts.
* @dev Implements additional functions to test internal functions of the yield streamer contract.
*/
contract YieldStreamerTestable is YieldStreamer {
// ------------------ Internal initializers ------------------- //

function call_parent_initialize(address underlyingToken) external {
__YieldStreamer_init(underlyingToken);
}

function call_parent_initialize_unchained(address underlyingToken) external {
__YieldStreamer_init_init_unchained(underlyingToken);
}

// ------------------ Setters for storage structures ---------- //

function setYieldState(address account, YieldState calldata newState) external {
_yieldStreamerStorage().yieldStates[account] = newState;
}

// ------------------ Getters for storage structures ---------- //

function getSourceGroupMapping(bytes32 groupKey) external view returns (uint256) {
return _yieldStreamerInitializationStorage().groupIds[groupKey];
}

// ------------------ Account initializers -------------------- //

function initializeSingleAccount(address account) external {
_initializeSingleAccount(account);
}

// ------------------ Yield calculation ----------------------- //

function getAccruePreview(
Expand Down Expand Up @@ -47,7 +76,6 @@ contract YieldStreamerTestable is YieldStreamer {
return _calculateSimpleYield(amount, rate, elapsedSeconds);
}


function inRangeYieldRates(
YieldRate[] memory rates,
uint256 fromTimestamp,
Expand Down Expand Up @@ -91,4 +119,18 @@ contract YieldStreamerTestable is YieldStreamer {
function map(AccruePreview memory accrue) external pure returns (ClaimPreview memory) {
return _map(accrue);
}

// ------------------ Bitwise functions ----------------------- //

function setBit(uint8 flags, uint256 bitIndex) external pure returns (uint8) {
return Bitwise.setBit(flags, bitIndex);
}

function clearBit(uint8 flags, uint256 bitIndex) external pure returns (uint8) {
return Bitwise.clearBit(flags, bitIndex);
}

function isBitSet(uint8 flags, uint256 bitIndex) external pure returns (bool) {
return Bitwise.isBitSet(flags, bitIndex);
}
}
8 changes: 4 additions & 4 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@ const config: HardhatUserConfig = {
accounts: process.env.CW_TESTNET_PK
? [process.env.CW_TESTNET_PK]
: {
mnemonic: process.env.CW_TESTNET_MNEMONIC ?? ""
}
mnemonic: process.env.CW_TESTNET_MNEMONIC ?? ""
}
},
cw_mainnet: {
url: process.env.CW_MAINNET_RPC,
accounts: process.env.CW_MAINNET_PK
? [process.env.CW_MAINNET_PK]
: {
mnemonic: process.env.CW_MAINNET_MNEMONIC ?? ""
}
mnemonic: process.env.CW_MAINNET_MNEMONIC ?? ""
}
}
},
gasReporter: {
Expand Down
29 changes: 29 additions & 0 deletions test-utils/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { network } from "hardhat";
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";

export async function setUpFixture<T>(func: () => Promise<T>): Promise<T> {
if (network.name === "hardhat") {
Expand All @@ -10,3 +11,31 @@ export async function setUpFixture<T>(func: () => Promise<T>): Promise<T> {
return func();
}
}

export function checkEquality<T extends Record<string, unknown>>(
actualObject: T,
expectedObject: T,
index?: number,
props: {
ignoreObjects: boolean;
} = { ignoreObjects: false }
) {
const indexString = index == null ? "" : ` with index: ${index}`;
Object.keys(expectedObject).forEach(property => {
const value = actualObject[property];
if (typeof value === "undefined" || typeof value === "function") {
throw Error(`Property "${property}" is not found in the actual object` + indexString);
}
if (typeof expectedObject[property] === "object" && props.ignoreObjects) {
return;
}
expect(value).to.eq(
expectedObject[property],
`Mismatch in the "${property}" property between the actual object and expected one` + indexString
);
});
}

export function maxUintForBits(numberOfBits: number): bigint {
return 2n ** BigInt(numberOfBits) - 1n;
}
46 changes: 25 additions & 21 deletions test-utils/eth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { ethers, upgrades } from "hardhat";
import { ethers, network, upgrades } from "hardhat";
import { BaseContract, BlockTag, Contract, ContractFactory, TransactionReceipt, TransactionResponse } from "ethers";
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
import { expect } from "chai";
import { time } from "@nomicfoundation/hardhat-network-helpers";

export async function proveTx(tx: Promise<TransactionResponse> | TransactionResponse): Promise<TransactionReceipt> {
const txResponse = await tx;
const txReceipt = await txResponse.wait();
if (!txReceipt) {
throw new Error("The transaction receipt is empty");
}
return txReceipt;
}

export async function checkContractUupsUpgrading(
contract: Contract,
Expand Down Expand Up @@ -37,6 +47,12 @@ export function getAddress(contract: Contract): string {
return address;
}

export async function getTxTimestamp(tx: Promise<TransactionResponse> | TransactionResponse): Promise<number> {
const receipt = await proveTx(tx);
const block = await ethers.provider.getBlock(receipt.blockNumber);
return Number(block?.timestamp ?? 0);
}

export async function getBlockTimestamp(blockTag: BlockTag): Promise<number> {
const block = await ethers.provider.getBlock(blockTag);
return block?.timestamp ?? 0;
Expand All @@ -46,25 +62,13 @@ export async function getLatestBlockTimestamp(): Promise<number> {
return getBlockTimestamp("latest");
}

export async function proveTx(txResponsePromise: Promise<TransactionResponse>): Promise<TransactionReceipt> {
const txResponse = await txResponsePromise;
const txReceipt = await txResponse.wait();
if (!txReceipt) {
throw new Error("The transaction receipt is empty");
export async function increaseBlockTimestampTo(target: number) {
if (network.name === "hardhat") {
await time.increaseTo(target);
} else if (network.name === "stratus") {
await ethers.provider.send("evm_setNextBlockTimestamp", [target]);
await ethers.provider.send("evm_mine", []);
} else {
throw new Error(`Setting block timestamp for the current blockchain is not supported: ${network.name}`);
}
return txReceipt as TransactionReceipt;
}

export function checkEquality<T extends Record<string, unknown>>(actualObject: T, expectedObject: T, index?: number) {
const indexString = !index ? "" : ` with index: ${index}`;
Object.keys(expectedObject).forEach(property => {
const value = actualObject[property];
if (typeof value === "undefined" || typeof value === "function" || typeof value === "object") {
throw Error(`Property "${property}" is not found in the actual object` + indexString);
}
expect(value).to.eq(
expectedObject[property],
`Mismatch in the "${property}" property between the actual object and expected one` + indexString
);
});
}
Loading

0 comments on commit d10fd31

Please sign in to comment.