Files
2025-12-27 21:15:45 -08:00

330 lines
13 KiB
TypeScript

import { FhevmType } from '@fhevm/hardhat-plugin';
import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers';
import { time } from '@nomicfoundation/hardhat-network-helpers';
import { expect } from 'chai';
import { ethers, fhevm } from 'hardhat';
const name = 'ConfidentialFungibleToken';
const symbol = 'CFT';
const uri = 'https://example.com/metadata';
/* eslint-disable no-unexpected-multiline */
describe('ERC7984Wrapper', function () {
beforeEach(async function () {
const accounts = await ethers.getSigners();
const [holder, recipient, operator] = accounts;
const token = await ethers.deployContract('ERC20Mock', ['Public Token', 'PT', 18]);
const wrapper = await ethers.deployContract('ERC7984ERC20WrapperMock', [token, name, symbol, uri]);
this.accounts = accounts.slice(3);
this.holder = holder;
this.recipient = recipient;
this.token = token;
this.operator = operator;
this.wrapper = wrapper;
await this.token.mint(this.holder.address, ethers.parseUnits('1000', 18));
await this.token.connect(this.holder).approve(this.wrapper, ethers.MaxUint256);
});
describe('Wrap', async function () {
for (const viaCallback of [false, true]) {
describe(`via ${viaCallback ? 'callback' : 'transfer from'}`, function () {
it('with multiple of rate', async function () {
const amountToWrap = ethers.parseUnits('100', 18);
if (viaCallback) {
await this.token.connect(this.holder).transferAndCall(this.wrapper, amountToWrap);
} else {
await this.wrapper.connect(this.holder).wrap(this.holder.address, amountToWrap);
}
await expect(this.token.balanceOf(this.holder)).to.eventually.equal(ethers.parseUnits('900', 18));
const wrappedBalanceHandle = await this.wrapper.confidentialBalanceOf(this.holder.address);
await expect(
fhevm.userDecryptEuint(FhevmType.euint64, wrappedBalanceHandle, this.wrapper.target, this.holder),
).to.eventually.equal(ethers.parseUnits('100', 6));
});
it('with value less than rate', async function () {
const amountToWrap = ethers.parseUnits('100', 8);
if (viaCallback) {
await this.token.connect(this.holder).transferAndCall(this.wrapper, amountToWrap);
} else {
await this.wrapper.connect(this.holder).wrap(this.holder.address, amountToWrap);
}
await expect(this.token.balanceOf(this.holder)).to.eventually.equal(ethers.parseUnits('1000', 18));
const wrappedBalanceHandle = await this.wrapper.confidentialBalanceOf(this.holder.address);
await expect(
fhevm.userDecryptEuint(FhevmType.euint64, wrappedBalanceHandle, this.wrapper.target, this.holder),
).to.eventually.equal(0);
});
it('with non-multiple of rate', async function () {
const amountToWrap = ethers.parseUnits('101', 11);
if (viaCallback) {
await this.token.connect(this.holder).transferAndCall(this.wrapper, amountToWrap);
} else {
await this.wrapper.connect(this.holder).wrap(this.holder.address, amountToWrap);
}
await expect(this.token.balanceOf(this.holder)).to.eventually.equal(
ethers.parseUnits('1000', 18) - ethers.parseUnits('10', 12),
);
const wrappedBalanceHandle = await this.wrapper.confidentialBalanceOf(this.holder.address);
await expect(
fhevm.userDecryptEuint(FhevmType.euint64, wrappedBalanceHandle, this.wrapper.target, this.holder),
).to.eventually.equal(10);
});
if (viaCallback) {
it('to another address', async function () {
const amountToWrap = ethers.parseUnits('100', 18);
await this.token
.connect(this.holder)
['transferAndCall(address,uint256,bytes)'](
this.wrapper,
amountToWrap,
ethers.solidityPacked(['address'], [this.recipient.address]),
);
await expect(this.token.balanceOf(this.holder)).to.eventually.equal(ethers.parseUnits('900', 18));
const wrappedBalanceHandle = await this.wrapper.confidentialBalanceOf(this.recipient.address);
await expect(
fhevm.userDecryptEuint(FhevmType.euint64, wrappedBalanceHandle, this.wrapper.target, this.recipient),
).to.eventually.equal(ethers.parseUnits('100', 6));
});
it('from unauthorized caller', async function () {
await expect(this.wrapper.connect(this.holder).onTransferReceived(this.holder, this.holder, 100, '0x'))
.to.be.revertedWithCustomError(this.wrapper, 'ERC7984UnauthorizedCaller')
.withArgs(this.holder.address);
});
}
});
}
});
describe('Unwrap', async function () {
beforeEach(async function () {
const amountToWrap = ethers.parseUnits('100', 18);
await this.token.connect(this.holder).transferAndCall(this.wrapper, amountToWrap);
});
it('less than balance', async function () {
const withdrawalAmount = ethers.parseUnits('10', 6);
const encryptedInput = await fhevm
.createEncryptedInput(this.wrapper.target, this.holder.address)
.add64(withdrawalAmount)
.encrypt();
await this.wrapper
.connect(this.holder)
['unwrap(address,address,bytes32,bytes)'](
this.holder,
this.holder,
encryptedInput.handles[0],
encryptedInput.inputProof,
);
await publicDecryptAndFinalizeUnwrap(this.wrapper, this.holder);
await expect(this.token.balanceOf(this.holder)).to.eventually.equal(
withdrawalAmount * 10n ** 12n + ethers.parseUnits('900', 18),
);
});
it('unwrap full balance', async function () {
await this.wrapper
.connect(this.holder)
.unwrap(this.holder, this.holder, await this.wrapper.confidentialBalanceOf(this.holder.address));
await publicDecryptAndFinalizeUnwrap(this.wrapper, this.holder);
await expect(this.token.balanceOf(this.holder)).to.eventually.equal(ethers.parseUnits('1000', 18));
});
it('more than balance', async function () {
const withdrawalAmount = ethers.parseUnits('101', 9);
const input = fhevm.createEncryptedInput(this.wrapper.target, this.holder.address);
input.add64(withdrawalAmount);
const encryptedInput = await input.encrypt();
await this.wrapper
.connect(this.holder)
['unwrap(address,address,bytes32,bytes)'](
this.holder,
this.holder,
encryptedInput.handles[0],
encryptedInput.inputProof,
);
await publicDecryptAndFinalizeUnwrap(this.wrapper, this.holder);
await expect(this.token.balanceOf(this.holder)).to.eventually.equal(ethers.parseUnits('900', 18));
});
it('to invalid recipient', async function () {
const withdrawalAmount = ethers.parseUnits('10', 9);
const input = fhevm.createEncryptedInput(this.wrapper.target, this.holder.address);
input.add64(withdrawalAmount);
const encryptedInput = await input.encrypt();
await expect(
this.wrapper
.connect(this.holder)
['unwrap(address,address,bytes32,bytes)'](
this.holder,
ethers.ZeroAddress,
encryptedInput.handles[0],
encryptedInput.inputProof,
),
)
.to.be.revertedWithCustomError(this.wrapper, 'ERC7984InvalidReceiver')
.withArgs(ethers.ZeroAddress);
});
it('via an approved operator', async function () {
const withdrawalAmount = ethers.parseUnits('100', 6);
const encryptedInput = await fhevm
.createEncryptedInput(this.wrapper.target, this.operator.address)
.add64(withdrawalAmount)
.encrypt();
await this.wrapper.connect(this.holder).setOperator(this.operator.address, (await time.latest()) + 1000);
await this.wrapper
.connect(this.operator)
['unwrap(address,address,bytes32,bytes)'](
this.holder,
this.holder,
encryptedInput.handles[0],
encryptedInput.inputProof,
);
await publicDecryptAndFinalizeUnwrap(this.wrapper, this.operator);
await expect(this.token.balanceOf(this.holder)).to.eventually.equal(ethers.parseUnits('1000', 18));
});
it('via an unapproved operator', async function () {
const withdrawalAmount = ethers.parseUnits('100', 9);
const input = fhevm.createEncryptedInput(this.wrapper.target, this.operator.address);
input.add64(withdrawalAmount);
const encryptedInput = await input.encrypt();
await expect(
this.wrapper
.connect(this.operator)
['unwrap(address,address,bytes32,bytes)'](
this.holder,
this.holder,
encryptedInput.handles[0],
encryptedInput.inputProof,
),
)
.to.be.revertedWithCustomError(this.wrapper, 'ERC7984UnauthorizedSpender')
.withArgs(this.holder, this.operator);
});
it('with a value not allowed to sender', async function () {
const totalSupplyHandle = await this.wrapper.confidentialTotalSupply();
await expect(this.wrapper.connect(this.holder).unwrap(this.holder, this.holder, totalSupplyHandle))
.to.be.revertedWithCustomError(this.wrapper, 'ERC7984UnauthorizedUseOfEncryptedAmount')
.withArgs(totalSupplyHandle, this.holder);
});
it('finalized with invalid signature', async function () {
const withdrawalAmount = ethers.parseUnits('10', 6);
const encryptedInput = await fhevm
.createEncryptedInput(this.wrapper.target, this.holder.address)
.add64(withdrawalAmount)
.encrypt();
await this.wrapper
.connect(this.holder)
['unwrap(address,address,bytes32,bytes)'](
this.holder,
this.holder,
encryptedInput.handles[0],
encryptedInput.inputProof,
);
const event = (await this.wrapper.queryFilter(this.wrapper.filters.UnwrapRequested()))[0];
const unwrapAmount = event.args[1];
const publicDecryptResults = await fhevm.publicDecrypt([unwrapAmount]);
await expect(
this.wrapper
.connect(this.holder)
.finalizeUnwrap(
unwrapAmount,
publicDecryptResults.abiEncodedClearValues,
publicDecryptResults.decryptionProof.slice(0, publicDecryptResults.decryptionProof.length - 2),
),
).to.be.reverted;
});
it('finalize invalid unwrap request', async function () {
await expect(
this.wrapper.connect(this.holder).finalizeUnwrap(ethers.ZeroHash, 0, '0x'),
).to.be.revertedWithCustomError(this.wrapper, 'InvalidUnwrapRequest');
});
});
describe('Initialization', function () {
describe('decimals', function () {
it('when underlying has 6 decimals', async function () {
const token = await ethers.deployContract('ERC20Mock', ['Public Token', 'PT', 6]);
const wrapper = await ethers.deployContract('ERC7984ERC20WrapperMock', [token, name, symbol, uri]);
await expect(wrapper.decimals()).to.eventually.equal(6);
await expect(wrapper.rate()).to.eventually.equal(1);
});
it('when underlying has more than 9 decimals', async function () {
const token = await ethers.deployContract('ERC20Mock', ['Public Token', 'PT', 18]);
const wrapper = await ethers.deployContract('ERC7984ERC20WrapperMock', [token, name, symbol, uri]);
await expect(wrapper.decimals()).to.eventually.equal(6);
await expect(wrapper.rate()).to.eventually.equal(10n ** 12n);
});
it('when underlying has less than 6 decimals', async function () {
const token = await ethers.deployContract('ERC20Mock', ['Public Token', 'PT', 4]);
const wrapper = await ethers.deployContract('ERC7984ERC20WrapperMock', [token, name, symbol, uri]);
await expect(wrapper.decimals()).to.eventually.equal(4);
await expect(wrapper.rate()).to.eventually.equal(1);
});
it('when underlying decimals are not available', async function () {
const token = await ethers.deployContract('ERC20RevertDecimalsMock');
const wrapper = await ethers.deployContract('ERC7984ERC20WrapperMock', [token, name, symbol, uri]);
await expect(wrapper.decimals()).to.eventually.equal(6);
await expect(wrapper.rate()).to.eventually.equal(10n ** 12n);
});
it('when decimals are over `type(uint8).max`', async function () {
const token = await ethers.deployContract('ERC20ExcessDecimalsMock');
await expect(ethers.deployContract('ERC7984ERC20WrapperMock', [token, name, symbol, uri])).to.be.reverted;
});
});
});
});
/* eslint-disable no-unexpected-multiline */
// Helper function to decrypt and finalize unwrap following OpenZeppelin pattern
async function publicDecryptAndFinalizeUnwrap(wrapper: any, caller: HardhatEthersSigner) {
const [to, amount] = (await wrapper.queryFilter(wrapper.filters.UnwrapRequested()))[0].args;
const { abiEncodedClearValues, decryptionProof } = await fhevm.publicDecrypt([amount]);
await expect(wrapper.connect(caller).finalizeUnwrap(amount, abiEncodedClearValues, decryptionProof))
.to.emit(wrapper, 'UnwrapFinalized')
.withArgs(to, amount, abiEncodedClearValues);
}