Skip to content

Commit f3a616a

Browse files
committed
ERC-7540 - Add ERC7540 - add unit tests
1 parent 30776ad commit f3a616a

File tree

2 files changed

+184
-2
lines changed

2 files changed

+184
-2
lines changed

contracts/mocks/token/ERC7540Mock.sol

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@ pragma solidity ^0.8.20;
55
import {IERC20} from "../../token/ERC20/IERC20.sol";
66
import {ERC20} from "../../token/ERC20/ERC20.sol";
77
import {ERC7540} from "../../token/ERC20/extensions/ERC7540.sol";
8-
import {ERC4626} from "../../token/ERC20/extensions/ERC4626.sol";
98

109
contract ERC7540Mock is ERC7540 {
11-
constructor(IERC20 asset) ERC20("ERC4626Mock", "E4626M") ERC7540(asset) {}
10+
constructor(IERC20 asset) ERC20("ERC7540Mock", "E7540M") ERC7540(asset) {}
1211

1312
function _processPendingRequests(uint256 requestId, address controller) internal view override {
1413
// ToDo
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
const { ethers } = require("hardhat");
2+
const { expect } = require("chai");
3+
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");
4+
5+
const name = "TestToken";
6+
const symbol = "TTKN";
7+
const decimals = 18n;
8+
const initialSupply = ethers.parseUnits("1000000", decimals);
9+
10+
async function fixture() {
11+
const [deployer, holder, recipient, controller, operator, other] = await ethers.getSigners();
12+
13+
const AssetToken = await ethers.getContractFactory("ERC20Mock");
14+
const assetToken = await AssetToken.deploy(name, symbol, decimals);
15+
await assetToken.mint(holder.address, initialSupply);
16+
17+
const ERC7540Vault = await ethers.getContractFactory("ERC7540Mock");
18+
const vault = await ERC7540Vault.deploy(assetToken.address);
19+
20+
return { deployer, holder, recipient, controller, operator, other, assetToken, vault };
21+
}
22+
23+
describe("ERC7540", function () {
24+
beforeEach(async function () {
25+
Object.assign(this, await loadFixture(fixture));
26+
});
27+
28+
describe("Deployment", function () {
29+
it("Should initialize the correct asset token", async function () {
30+
expect(await this.vault.asset()).to.equal(this.assetToken.target);
31+
});
32+
33+
it("Should set the correct initial decimals", async function () {
34+
expect(await this.vault.decimals()).to.equal(decimals);
35+
});
36+
});
37+
38+
describe("Request Deposit", function () {
39+
it("Should create a deposit request", async function () {
40+
const depositAmount = ethers.parseUnits("100", decimals);
41+
await this.assetToken.connect(this.holder).approve(this.vault.target, depositAmount);
42+
43+
const requestId = await this.vault
44+
.connect(this.holder)
45+
.requestDeposit(depositAmount, this.controller.address, this.holder.address);
46+
47+
expect(await this.vault.pendingDepositRequest(requestId, this.controller.address)).to.equal(depositAmount);
48+
});
49+
50+
it("Should revert if deposit amount is zero", async function () {
51+
await expect(
52+
this.vault.connect(this.holder).requestDeposit(0, this.controller.address, this.holder.address)
53+
).to.be.revertedWithCustomError(this.vault, "ERC7540ZeroAssetsNotAllowed");
54+
});
55+
56+
it("Should revert if sender is not authorized", async function () {
57+
const depositAmount = ethers.parseUnits("100", decimals);
58+
await this.assetToken.connect(this.holder).approve(this.vault.target, depositAmount);
59+
60+
await expect(
61+
this.vault.connect(this.other).requestDeposit(depositAmount, this.controller.address, this.holder.address)
62+
).to.be.revertedWithCustomError(this.vault, "ERC7540Unauthorized");
63+
});
64+
});
65+
66+
describe("Request Redeem", function () {
67+
it("Should create a redeem request", async function () {
68+
const redeemAmount = ethers.parseUnits("50", decimals);
69+
await this.assetToken.connect(this.holder).approve(this.vault.target, redeemAmount);
70+
await this.vault.connect(this.holder).requestDeposit(redeemAmount, this.controller.address, this.holder.address);
71+
72+
const requestId = await this.vault
73+
.connect(this.holder)
74+
.requestRedeem(redeemAmount, this.controller.address, this.holder.address);
75+
76+
expect(await this.vault.pendingRedeemRequest(requestId, this.controller.address)).to.equal(redeemAmount);
77+
});
78+
79+
it("Should revert if redeem amount is zero", async function () {
80+
await expect(
81+
this.vault.connect(this.holder).requestRedeem(0, this.controller.address, this.holder.address)
82+
).to.be.revertedWithCustomError(this.vault, "ERC7540ZeroSharesNotAllowed");
83+
});
84+
85+
it("Should revert if sender is not authorized", async function () {
86+
const redeemAmount = ethers.parseUnits("50", decimals);
87+
await this.assetToken.connect(this.holder).approve(this.vault.target, redeemAmount);
88+
await this.vault.connect(this.holder).requestDeposit(redeemAmount, this.controller.address, this.holder.address);
89+
90+
await expect(
91+
this.vault.connect(this.other).requestRedeem(redeemAmount, this.controller.address, this.holder.address)
92+
).to.be.revertedWithCustomError(this.vault, "ERC7540Unauthorized");
93+
});
94+
});
95+
96+
describe("Claim Deposits and Redemptions", function () {
97+
beforeEach(async function () {
98+
this.depositAmount = ethers.parseUnits("200", decimals);
99+
await this.assetToken.connect(this.holder).approve(this.vault.target, this.depositAmount);
100+
await this.vault.connect(this.holder).requestDeposit(this.depositAmount, this.controller.address, this.holder.address);
101+
});
102+
103+
it("Should allow a controller to claim a deposit", async function () {
104+
await expect(
105+
this.vault.connect(this.controller).deposit(this.depositAmount, this.holder.address, this.controller.address)
106+
).to.not.be.reverted;
107+
});
108+
109+
it("Should revert if the controller tries to claim more than claimable amount", async function () {
110+
await expect(
111+
this.vault.connect(this.controller).deposit(this.depositAmount + 1n, this.holder.address, this.controller.address)
112+
).to.be.revertedWithCustomError(this.vault, "ERC7540InsufficientClaimable");
113+
});
114+
});
115+
116+
describe("Operator Permissions", function () {
117+
it("Should allow a user to set an operator", async function () {
118+
await expect(this.vault.connect(this.holder).setOperator(this.operator.address, true))
119+
.to.emit(this.vault, "OperatorSet")
120+
.withArgs(this.holder.address, this.operator.address, true);
121+
122+
expect(await this.vault.isOperator(this.holder.address, this.operator.address)).to.be.true;
123+
});
124+
125+
it("Should allow an operator to perform actions on behalf of the owner", async function () {
126+
await this.vault.connect(this.holder).setOperator(this.operator.address, true);
127+
128+
const depositAmount = ethers.parseUnits("50", decimals);
129+
await this.assetToken.connect(this.holder).approve(this.vault.target, depositAmount);
130+
131+
await expect(
132+
this.vault.connect(this.operator).requestDeposit(depositAmount, this.controller.address, this.holder.address)
133+
).to.not.be.reverted;
134+
});
135+
136+
it("Should revert if an unapproved operator tries to perform an action", async function () {
137+
await expect(
138+
this.vault.connect(this.other).requestDeposit(100, this.controller.address, this.holder.address)
139+
).to.be.revertedWithCustomError(this.vault, "ERC7540Unauthorized");
140+
});
141+
});
142+
143+
describe("ERC-165 Interface Support", function () {
144+
it("Should support ERC-165", async function () {
145+
expect(await this.vault.supportsInterface("0x01ffc9a7")).to.be.true; // ERC-165 ID
146+
});
147+
148+
it("Should support ERC-7540 operator methods", async function () {
149+
expect(await this.vault.supportsInterface("0xe3bc4e65")).to.be.true;
150+
});
151+
152+
it("Should support ERC-7575", async function () {
153+
expect(await this.vault.supportsInterface("0x2f0a18c5")).to.be.true;
154+
});
155+
156+
it("Should support async deposit vault", async function () {
157+
expect(await this.vault.supportsInterface("0xce3bbe50")).to.be.true;
158+
});
159+
160+
it("Should support async redemption vault", async function () {
161+
expect(await this.vault.supportsInterface("0x620ee8e4")).to.be.true;
162+
});
163+
164+
it("Should return false for unsupported interfaces", async function () {
165+
expect(await this.vault.supportsInterface("0x00000000")).to.be.false;
166+
});
167+
});
168+
169+
describe("Reentrancy and Edge Cases", function () {
170+
it("Should prevent reentrancy attacks", async function () {
171+
// Implement tests to check for reentrancy if using a vulnerable function
172+
});
173+
174+
it("Should handle large request amounts", async function () {
175+
const largeAmount = ethers.parseUnits("1000000000", decimals);
176+
await this.assetToken.connect(this.holder).approve(this.vault.target, largeAmount);
177+
178+
await expect(
179+
this.vault.connect(this.holder).requestDeposit(largeAmount, this.controller.address, this.holder.address)
180+
).to.not.be.reverted;
181+
});
182+
});
183+
});

0 commit comments

Comments
 (0)