diff --git a/contracts/mocks/token/ERC7984FreezableMock.sol b/contracts/mocks/token/ERC7984FreezableMock.sol index 2c1e90ec..bc5d4973 100644 --- a/contracts/mocks/token/ERC7984FreezableMock.sol +++ b/contracts/mocks/token/ERC7984FreezableMock.sol @@ -28,7 +28,7 @@ contract ERC7984FreezableMock is ERC7984Mock, ERC7984Freezable, AccessControl, H address from, address to, euint64 amount - ) internal virtual override(ERC7984Mock, ERC7984Freezable) returns (euint64) { + ) internal virtual override(ERC7984, ERC7984Freezable) returns (euint64) { return super._update(from, to, amount); } diff --git a/contracts/mocks/token/ERC7984Mock.sol b/contracts/mocks/token/ERC7984Mock.sol index 7288eb91..72649460 100644 --- a/contracts/mocks/token/ERC7984Mock.sol +++ b/contracts/mocks/token/ERC7984Mock.sol @@ -17,9 +17,9 @@ contract ERC7984Mock is ERC7984, SepoliaConfig { _OWNER = msg.sender; } - function _update(address from, address to, euint64 amount) internal virtual override returns (euint64 transferred) { - transferred = super._update(from, to, amount); - FHE.allow(confidentialTotalSupply(), _OWNER); + function confidentialTotalSupplyAccess() public { + require(msg.sender == _OWNER); + FHE.allow(confidentialTotalSupply(), msg.sender); } function $_mint( diff --git a/contracts/mocks/token/ERC7984ObserverAccessMock.sol b/contracts/mocks/token/ERC7984ObserverAccessMock.sol index b21e18f5..c1e65f62 100644 --- a/contracts/mocks/token/ERC7984ObserverAccessMock.sol +++ b/contracts/mocks/token/ERC7984ObserverAccessMock.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.27; import {SepoliaConfig} from "@fhevm/solidity/config/ZamaConfig.sol"; import {FHE, euint64, externalEuint64} from "@fhevm/solidity/lib/FHE.sol"; +import {ERC7984} from "../../token/ERC7984/ERC7984.sol"; import {ERC7984ObserverAccess} from "../../token/ERC7984/extensions/ERC7984ObserverAccess.sol"; import {ERC7984Mock} from "./ERC7984Mock.sol"; @@ -18,7 +19,7 @@ contract ERC7984ObserverAccessMock is ERC7984ObserverAccess, ERC7984Mock { address from, address to, euint64 amount - ) internal virtual override(ERC7984ObserverAccess, ERC7984Mock) returns (euint64) { + ) internal virtual override(ERC7984ObserverAccess, ERC7984) returns (euint64) { return super._update(from, to, amount); } } diff --git a/contracts/mocks/token/ERC7984VotesMock.sol b/contracts/mocks/token/ERC7984VotesMock.sol index 09c61cf4..886cd825 100644 --- a/contracts/mocks/token/ERC7984VotesMock.sol +++ b/contracts/mocks/token/ERC7984VotesMock.sol @@ -30,11 +30,16 @@ abstract contract ERC7984VotesMock is ERC7984Mock, ERC7984Votes { return super.confidentialTotalSupply(); } + function getPastTotalSupplyAccess(uint256 timepoint) public { + require(msg.sender == _OWNER); + FHE.allow(getPastTotalSupply(timepoint), msg.sender); + } + function _update( address from, address to, euint64 amount - ) internal virtual override(ERC7984Mock, ERC7984Votes) returns (euint64) { + ) internal virtual override(ERC7984, ERC7984Votes) returns (euint64) { return super._update(from, to, amount); } diff --git a/contracts/token/ERC7984/extensions/ERC7984Freezable.sol b/contracts/token/ERC7984/extensions/ERC7984Freezable.sol index 50fe89d5..3191ab77 100644 --- a/contracts/token/ERC7984/extensions/ERC7984Freezable.sol +++ b/contracts/token/ERC7984/extensions/ERC7984Freezable.sol @@ -7,7 +7,7 @@ import {FHESafeMath} from "../../../utils/FHESafeMath.sol"; import {ERC7984} from "../ERC7984.sol"; /** - * @dev Extension of {ERC7984} that implements a confidential + * @dev Extension of {ERC7984} that allows to implement a confidential * freezing mechanism that can be managed by an authorized account with * {setConfidentialFrozen} functions. * @@ -31,12 +31,8 @@ abstract contract ERC7984Freezable is ERC7984 { } /// @dev Returns the confidential available (unfrozen) balance of an account. Up to {confidentialBalanceOf}. - function confidentialAvailable(address account) public virtual returns (euint64) { - (ebool success, euint64 unfrozen) = FHESafeMath.tryDecrease( - confidentialBalanceOf(account), - confidentialFrozen(account) - ); - return FHE.select(success, unfrozen, FHE.asEuint64(0)); + function confidentialAvailable(address account) public virtual returns (euint64 unfrozen) { + (, unfrozen) = FHESafeMath.tryDecrease(confidentialBalanceOf(account), confidentialFrozen(account)); } /// @dev Freezes a confidential amount of tokens for an account with a proof. diff --git a/contracts/utils/FHESafeMath.sol b/contracts/utils/FHESafeMath.sol index e184413c..6cf164e9 100644 --- a/contracts/utils/FHESafeMath.sol +++ b/contracts/utils/FHESafeMath.sol @@ -31,7 +31,15 @@ library FHESafeMath { * and `updated` will be the original value. */ function tryDecrease(euint64 oldValue, euint64 delta) internal returns (ebool success, euint64 updated) { - success = FHE.ge(oldValue, delta); - updated = FHE.select(success, FHE.sub(oldValue, delta), oldValue); + if (!FHE.isInitialized(oldValue)) { + success = FHE.asEbool(false); + updated = FHE.asEuint64(0); + } else if (!FHE.isInitialized(delta)) { + success = FHE.asEbool(true); + updated = oldValue; + } else { + success = FHE.ge(oldValue, delta); + updated = FHE.select(success, FHE.sub(oldValue, delta), oldValue); + } } } diff --git a/test/token/ERC7984/ERC7984.behaviour.ts b/test/token/ERC7984/ERC7984.behaviour.ts new file mode 100644 index 00000000..1d1fa753 --- /dev/null +++ b/test/token/ERC7984/ERC7984.behaviour.ts @@ -0,0 +1,433 @@ +import { ERC7984ReceiverMock } from '../../../types'; +import { $ERC7984Mock } from '../../../types/contracts-exposed/mocks/token/ERC7984Mock.sol/$ERC7984Mock'; +import { allowHandle } from '../../helpers/accounts'; +import { deployERC7984Fixture } from './ERC7984.test'; +import { FhevmType } from '@fhevm/hardhat-plugin'; +import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers'; +import { expect } from 'chai'; +import hre, { ethers, fhevm } from 'hardhat'; + +const name = 'ConfidentialFungibleToken'; +const symbol = 'CFT'; +const uri = 'https://example.com/metadata'; + +function shouldBehaveLikeERC7984(contract?: string, ...extraDeploymentArgs: any[]) { + const deployFixture = () => deployERC7984Fixture(contract, extraDeploymentArgs); + + describe('ERC7984 behaviour', function () { + describe('constructor', function () { + it('sets the name', async function () { + const { token } = await deployFixture(); + await expect(token.name()).to.eventually.equal(name); + }); + + it('sets the symbol', async function () { + const { token } = await deployFixture(); + await expect(token.symbol()).to.eventually.equal(symbol); + }); + + it('sets the uri', async function () { + const { token } = await deployFixture(); + await expect(token.tokenURI()).to.eventually.equal(uri); + }); + + it('decimals is 6', async function () { + const { token } = await deployFixture(); + await expect(token.decimals()).to.eventually.equal(6); + }); + }); + + describe('confidentialBalanceOf', function () { + it('handle can be reencryped by owner', async function () { + const { token, holder } = await deployFixture(); + const balanceOfHandleHolder = await token.confidentialBalanceOf(holder); + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, balanceOfHandleHolder, await token.getAddress(), holder), + ).to.eventually.equal(1000); + }); + + it('handle cannot be reencryped by non-owner', async function () { + const { token, holder, anyone } = await deployFixture(); + const balanceOfHandleHolder = await token.confidentialBalanceOf(holder); + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, balanceOfHandleHolder, await token.getAddress(), anyone), + ).to.be.rejectedWith(generateReencryptionErrorMessage(balanceOfHandleHolder, anyone.address)); + }); + }); + + describe('transfer', function () { + for (const asSender of [true, false]) { + describe(asSender ? 'as sender' : 'as operator', function () { + let [holder, recipient, operator]: HardhatEthersSigner[] = []; + let token: $ERC7984Mock; + beforeEach(async function () { + ({ token, holder, recipient, operator } = await deployFixture()); + if (!asSender) { + const timestamp = (await ethers.provider.getBlock('latest'))!.timestamp + 100; + await token.connect(holder).setOperator(operator.address, timestamp); + } + }); + + if (!asSender) { + for (const withCallback of [false, true]) { + describe(withCallback ? 'with callback' : 'without callback', function () { + let encryptedInput: any; + let params: any; + + beforeEach(async function () { + encryptedInput = await fhevm + .createEncryptedInput(await token.getAddress(), operator.address) + .add64(100) + .encrypt(); + + params = [holder.address, recipient.address, encryptedInput.handles[0], encryptedInput.inputProof]; + if (withCallback) { + params.push('0x'); + } + }); + + it('without operator approval should fail', async function () { + await token.$_setOperator(holder, operator, 0); + + await expect( + token + .connect(operator) + [ + withCallback + ? 'confidentialTransferFromAndCall(address,address,bytes32,bytes,bytes)' + : 'confidentialTransferFrom(address,address,bytes32,bytes)' + ](...params), + ) + .to.be.revertedWithCustomError(token, 'ERC7984UnauthorizedSpender') + .withArgs(holder.address, operator.address); + }); + + it('should be successful', async function () { + await token + .connect(operator) + [ + withCallback + ? 'confidentialTransferFromAndCall(address,address,bytes32,bytes,bytes)' + : 'confidentialTransferFrom(address,address,bytes32,bytes)' + ](...params); + }); + }); + } + } + + // Edge cases to run with sender as caller + if (asSender) { + it('with no balance should revert', async function () { + const encryptedInput = await fhevm + .createEncryptedInput(await token.getAddress(), recipient.address) + .add64(100) + .encrypt(); + + await expect( + token + .connect(recipient) + ['confidentialTransfer(address,bytes32,bytes)']( + holder.address, + encryptedInput.handles[0], + encryptedInput.inputProof, + ), + ) + .to.be.revertedWithCustomError(token, 'ERC7984ZeroBalance') + .withArgs(recipient.address); + }); + + it('to zero address', async function () { + const encryptedInput = await fhevm + .createEncryptedInput(await token.getAddress(), holder.address) + .add64(100) + .encrypt(); + + await expect( + token + .connect(holder) + ['confidentialTransfer(address,bytes32,bytes)']( + ethers.ZeroAddress, + encryptedInput.handles[0], + encryptedInput.inputProof, + ), + ) + .to.be.revertedWithCustomError(token, 'ERC7984InvalidReceiver') + .withArgs(ethers.ZeroAddress); + }); + } + + for (const sufficientBalance of [false, true]) { + it(`${sufficientBalance ? 'sufficient' : 'insufficient'} balance`, async function () { + const transferAmount = sufficientBalance ? 400 : 1100; + + const encryptedInput = await fhevm + .createEncryptedInput(await token.getAddress(), asSender ? holder.address : operator.address) + .add64(transferAmount) + .encrypt(); + + let tx; + if (asSender) { + tx = await token + .connect(holder) + ['confidentialTransfer(address,bytes32,bytes)']( + recipient.address, + encryptedInput.handles[0], + encryptedInput.inputProof, + ); + } else { + tx = await token + .connect(operator) + ['confidentialTransferFrom(address,address,bytes32,bytes)']( + holder.address, + recipient.address, + encryptedInput.handles[0], + encryptedInput.inputProof, + ); + } + const transferEvent = (await tx.wait()).logs.filter((log: any) => log.address === token.target)[0]; + expect(transferEvent.args[0]).to.equal(holder.address); + expect(transferEvent.args[1]).to.equal(recipient.address); + + const transferAmountHandle = transferEvent.args[2]; + const holderBalanceHandle = await token.confidentialBalanceOf(holder); + const recipientBalanceHandle = await token.confidentialBalanceOf(recipient); + + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, transferAmountHandle, await token.getAddress(), holder), + ).to.eventually.equal(sufficientBalance ? transferAmount : 0); + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, transferAmountHandle, await token.getAddress(), recipient), + ).to.eventually.equal(sufficientBalance ? transferAmount : 0); + // Other can not reencrypt the transfer amount + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, transferAmountHandle, await token.getAddress(), operator), + ).to.be.rejectedWith(generateReencryptionErrorMessage(transferAmountHandle, operator.address)); + + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, holderBalanceHandle, await token.getAddress(), holder), + ).to.eventually.equal(1000 - (sufficientBalance ? transferAmount : 0)); + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, recipientBalanceHandle, await token.getAddress(), recipient), + ).to.eventually.equal(sufficientBalance ? transferAmount : 0); + }); + } + }); + } + + describe('without input proof', function () { + for (const [usingTransferFrom, withCallback] of [false, true].flatMap(val => [ + [val, false], + [val, true], + ])) { + describe(`using ${usingTransferFrom ? 'confidentialTransferFrom' : 'confidentialTransfer'} ${ + withCallback ? 'with callback' : '' + }`, function () { + async function callTransfer(contract: any, from: any, to: any, amount: any, sender: any = from) { + let functionParams = [to, amount]; + + if (withCallback) { + functionParams.push('0x'); + if (usingTransferFrom) { + functionParams.unshift(from); + await contract.connect(sender).confidentialTransferFromAndCall(...functionParams); + } else { + await contract.connect(sender).confidentialTransferAndCall(...functionParams); + } + } else { + if (usingTransferFrom) { + functionParams.unshift(from); + await contract.connect(sender).confidentialTransferFrom(...functionParams); + } else { + await contract.connect(sender).confidentialTransfer(...functionParams); + } + } + } + + it('full balance', async function () { + const { token, holder, recipient } = await deployFixture(); + const fullBalanceHandle = await token.confidentialBalanceOf(holder); + + await callTransfer(token, holder, recipient, fullBalanceHandle); + + await expect( + fhevm.userDecryptEuint( + FhevmType.euint64, + await token.confidentialBalanceOf(recipient), + await token.getAddress(), + recipient, + ), + ).to.eventually.equal(1000); + }); + + it('other user balance should revert', async function () { + const { token, holder, recipient } = await deployFixture(); + const encryptedInput = await fhevm + .createEncryptedInput(await token.getAddress(), holder.address) + .add64(100) + .encrypt(); + + await token + .connect(holder) + ['$_mint(address,bytes32,bytes)'](recipient, encryptedInput.handles[0], encryptedInput.inputProof); + + const recipientBalanceHandle = await token.confidentialBalanceOf(recipient); + await expect(callTransfer(token, holder, recipient, recipientBalanceHandle)) + .to.be.revertedWithCustomError(token, 'ERC7984UnauthorizedUseOfEncryptedAmount') + .withArgs(recipientBalanceHandle, holder); + }); + + if (usingTransferFrom) { + describe('without operator approval', function () { + let [holder, recipient, operator]: HardhatEthersSigner[] = []; + let token: $ERC7984Mock; + beforeEach(async function () { + ({ token, holder, recipient, operator } = await deployFixture()); + await token.connect(holder).setOperator(operator.address, 0); + await allowHandle(hre, holder, operator, await token.confidentialBalanceOf(holder)); + }); + + it('should revert', async function () { + await expect( + callTransfer(token, holder, recipient, await token.confidentialBalanceOf(holder), operator), + ) + .to.be.revertedWithCustomError(token, 'ERC7984UnauthorizedSpender') + .withArgs(holder.address, operator.address); + }); + }); + } + }); + } + }); + + it('internal function reverts on from address zero', async function () { + const { token, holder, recipient } = await deployFixture(); + const encryptedInput = await fhevm + .createEncryptedInput(await token.getAddress(), holder.address) + .add64(100) + .encrypt(); + + await expect( + token + .connect(holder) + ['$_transfer(address,address,bytes32,bytes)']( + ethers.ZeroAddress, + recipient.address, + encryptedInput.handles[0], + encryptedInput.inputProof, + ), + ) + .to.be.revertedWithCustomError(token, 'ERC7984InvalidSender') + .withArgs(ethers.ZeroAddress); + }); + }); + + describe('transfer with callback', function () { + let [holder, recipient]: HardhatEthersSigner[] = []; + let token: $ERC7984Mock; + let recipientContract: ERC7984ReceiverMock; + let encryptedInput: any; + beforeEach(async function () { + ({ token, holder, recipient } = await deployFixture()); + recipientContract = await ethers.deployContract('ERC7984ReceiverMock'); + + encryptedInput = await fhevm + .createEncryptedInput(await token.getAddress(), holder.address) + .add64(1000) + .encrypt(); + }); + + for (const callbackSuccess of [false, true]) { + it(`with callback running ${callbackSuccess ? 'successfully' : 'unsuccessfully'}`, async function () { + const tx = await token + .connect(holder) + ['confidentialTransferAndCall(address,bytes32,bytes,bytes)']( + recipientContract.target, + encryptedInput.handles[0], + encryptedInput.inputProof, + ethers.AbiCoder.defaultAbiCoder().encode(['bool'], [callbackSuccess]), + ); + + await expect( + fhevm.userDecryptEuint( + FhevmType.euint64, + await token.confidentialBalanceOf(holder), + await token.getAddress(), + holder, + ), + ).to.eventually.equal(callbackSuccess ? 0 : 1000); + + // Verify event contents + expect(tx).to.emit(recipientContract, 'ConfidentialTransferCallback').withArgs(callbackSuccess); + const transferEvents = (await tx.wait()).logs.filter((log: any) => log.address === token.target); + + const outboundTransferEvent = transferEvents[0]; + const inboundTransferEvent = transferEvents[1]; + + expect(outboundTransferEvent.args[0]).to.equal(holder.address); + expect(outboundTransferEvent.args[1]).to.equal(recipientContract.target); + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, outboundTransferEvent.args[2], await token.getAddress(), holder), + ).to.eventually.equal(1000); + + expect(inboundTransferEvent.args[0]).to.equal(recipientContract.target); + expect(inboundTransferEvent.args[1]).to.equal(holder.address); + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, inboundTransferEvent.args[2], await token.getAddress(), holder), + ).to.eventually.equal(callbackSuccess ? 0 : 1000); + }); + } + + it('with callback reverting without a reason', async function () { + await expect( + token + .connect(holder) + ['confidentialTransferAndCall(address,bytes32,bytes,bytes)']( + recipientContract.target, + encryptedInput.handles[0], + encryptedInput.inputProof, + '0x', + ), + ) + .to.be.revertedWithCustomError(token, 'ERC7984InvalidReceiver') + .withArgs(recipientContract.target); + }); + + it('with callback reverting with a custom error', async function () { + await expect( + token + .connect(holder) + ['confidentialTransferAndCall(address,bytes32,bytes,bytes)']( + recipientContract.target, + encryptedInput.handles[0], + encryptedInput.inputProof, + ethers.AbiCoder.defaultAbiCoder().encode(['uint8'], [2]), + ), + ) + .to.be.revertedWithCustomError(recipientContract, 'InvalidInput') + .withArgs(2); + }); + + it('to an EOA', async function () { + await token + .connect(holder) + ['confidentialTransferAndCall(address,bytes32,bytes,bytes)']( + recipient, + encryptedInput.handles[0], + encryptedInput.inputProof, + '0x', + ); + + const balanceOfHandle = await token.confidentialBalanceOf(recipient); + await expect( + fhevm.userDecryptEuint(FhevmType.euint64, balanceOfHandle, await token.getAddress(), recipient), + ).to.eventually.equal(1000); + }); + }); + }); +} + +function generateReencryptionErrorMessage(handle: string, account: string): string { + return `User ${account} is not authorized to user decrypt handle ${handle}`; +} + +export { shouldBehaveLikeERC7984 }; diff --git a/test/token/ERC7984/ERC7984.test.ts b/test/token/ERC7984/ERC7984.test.ts index d28fcbb7..c8810100 100644 --- a/test/token/ERC7984/ERC7984.test.ts +++ b/test/token/ERC7984/ERC7984.test.ts @@ -1,108 +1,76 @@ -import { allowHandle } from '../../helpers/accounts'; +import { $ERC7984Mock } from '../../../types/contracts-exposed/mocks/token/ERC7984Mock.sol/$ERC7984Mock'; +import { shouldBehaveLikeERC7984 } from './ERC7984.behaviour'; import { FhevmType } from '@fhevm/hardhat-plugin'; +import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers'; import { expect } from 'chai'; -import hre, { ethers, fhevm } from 'hardhat'; +import { ethers, fhevm } from 'hardhat'; +const contract = '$ERC7984Mock'; const name = 'ConfidentialFungibleToken'; const symbol = 'CFT'; const uri = 'https://example.com/metadata'; -/* eslint-disable no-unexpected-multiline */ -describe('ERC7984', function () { - beforeEach(async function () { - const accounts = await ethers.getSigners(); - const [holder, recipient, operator] = accounts; - - const token = await ethers.deployContract('$ERC7984Mock', [name, symbol, uri]); - this.accounts = accounts.slice(3); - this.holder = holder; - this.recipient = recipient; - this.token = token; - this.operator = operator; - - const encryptedInput = await fhevm - .createEncryptedInput(this.token.target, this.holder.address) - .add64(1000) - .encrypt(); - - await this.token - .connect(this.holder) - ['$_mint(address,bytes32,bytes)'](this.holder, encryptedInput.handles[0], encryptedInput.inputProof); - }); - - describe('constructor', function () { - it('sets the name', async function () { - await expect(this.token.name()).to.eventually.equal(name); - }); - - it('sets the symbol', async function () { - await expect(this.token.symbol()).to.eventually.equal(symbol); - }); - - it('sets the uri', async function () { - await expect(this.token.tokenURI()).to.eventually.equal(uri); - }); - - it('decimals is 6', async function () { - await expect(this.token.decimals()).to.eventually.equal(6); - }); - }); - - describe('confidentialBalanceOf', function () { - it('handle can be reencryped by owner', async function () { - const balanceOfHandleHolder = await this.token.confidentialBalanceOf(this.holder); - await expect( - fhevm.userDecryptEuint(FhevmType.euint64, balanceOfHandleHolder, this.token.target, this.holder), - ).to.eventually.equal(1000); - }); - - it('handle cannot be reencryped by non-owner', async function () { - const balanceOfHandleHolder = await this.token.confidentialBalanceOf(this.holder); - await expect( - fhevm.userDecryptEuint(FhevmType.euint64, balanceOfHandleHolder, this.token.target, this.accounts[0]), - ).to.be.rejectedWith(generateReencryptionErrorMessage(balanceOfHandleHolder, this.accounts[0].address)); - }); - }); +async function deployFixture(_contract?: string, extraDeploymentArgs: any[] = []) { + const [holder, recipient, operator, anyone] = await ethers.getSigners(); + const token = (await ethers.deployContract(_contract ? _contract : contract, [ + name, + symbol, + uri, + ...extraDeploymentArgs, + ])) as any as $ERC7984Mock; + const encryptedInput = await fhevm + .createEncryptedInput(await token.getAddress(), holder.address) + .add64(1000) + .encrypt(); + await token + .connect(holder) + ['$_mint(address,bytes32,bytes)'](holder, encryptedInput.handles[0], encryptedInput.inputProof); + return { token, holder, recipient, operator, anyone }; +} +describe('ERC7984', function () { describe('mint', function () { for (const existingUser of [false, true]) { it(`to ${existingUser ? 'existing' : 'new'} user`, async function () { + const { token, holder } = await deployFixture(); if (existingUser) { const encryptedInput = await fhevm - .createEncryptedInput(this.token.target, this.holder.address) + .createEncryptedInput(await token.getAddress(), holder.address) .add64(1000) .encrypt(); - await this.token - .connect(this.holder) - ['$_mint(address,bytes32,bytes)'](this.holder, encryptedInput.handles[0], encryptedInput.inputProof); + await token + .connect(holder) + ['$_mint(address,bytes32,bytes)'](holder, encryptedInput.handles[0], encryptedInput.inputProof); } - const balanceOfHandleHolder = await this.token.confidentialBalanceOf(this.holder); + const balanceOfHandleHolder = await token.confidentialBalanceOf(holder); await expect( - fhevm.userDecryptEuint(FhevmType.euint64, balanceOfHandleHolder, this.token.target, this.holder), + fhevm.userDecryptEuint(FhevmType.euint64, balanceOfHandleHolder, await token.getAddress(), holder), ).to.eventually.equal(existingUser ? 2000 : 1000); // Check total supply - const totalSupplyHandle = await this.token.confidentialTotalSupply(); + const totalSupplyHandle = await token.confidentialTotalSupply(); + await token.connect(holder).confidentialTotalSupplyAccess(); await expect( - fhevm.userDecryptEuint(FhevmType.euint64, totalSupplyHandle, this.token.target, this.holder), + fhevm.userDecryptEuint(FhevmType.euint64, totalSupplyHandle, await token.getAddress(), holder), ).to.eventually.equal(existingUser ? 2000 : 1000); }); } it('from zero address', async function () { + const { token, holder } = await deployFixture(); const encryptedInput = await fhevm - .createEncryptedInput(this.token.target, this.holder.address) + .createEncryptedInput(await token.getAddress(), holder.address) .add64(400) .encrypt(); await expect( - this.token - .connect(this.holder) + token + .connect(holder) ['$_mint(address,bytes32,bytes)'](ethers.ZeroAddress, encryptedInput.handles[0], encryptedInput.inputProof), ) - .to.be.revertedWithCustomError(this.token, 'ERC7984InvalidReceiver') + .to.be.revertedWithCustomError(token, 'ERC7984InvalidReceiver') .withArgs(ethers.ZeroAddress); }); }); @@ -110,425 +78,64 @@ describe('ERC7984', function () { describe('burn', function () { for (const sufficientBalance of [false, true]) { it(`from a user with ${sufficientBalance ? 'sufficient' : 'insufficient'} balance`, async function () { + const { token, holder } = await deployFixture(); const burnAmount = sufficientBalance ? 400 : 1100; const encryptedInput = await fhevm - .createEncryptedInput(this.token.target, this.holder.address) + .createEncryptedInput(await token.getAddress(), holder.address) .add64(burnAmount) .encrypt(); - await this.token - .connect(this.holder) - ['$_burn(address,bytes32,bytes)'](this.holder, encryptedInput.handles[0], encryptedInput.inputProof); + await token + .connect(holder) + ['$_burn(address,bytes32,bytes)'](holder, encryptedInput.handles[0], encryptedInput.inputProof); - const balanceOfHandleHolder = await this.token.confidentialBalanceOf(this.holder); + const balanceOfHandleHolder = await token.confidentialBalanceOf(holder); await expect( - fhevm.userDecryptEuint(FhevmType.euint64, balanceOfHandleHolder, this.token.target, this.holder), + fhevm.userDecryptEuint(FhevmType.euint64, balanceOfHandleHolder, await token.getAddress(), holder), ).to.eventually.equal(sufficientBalance ? 600 : 1000); // Check total supply - const totalSupplyHandle = await this.token.confidentialTotalSupply(); + const totalSupplyHandle = await token.confidentialTotalSupply(); + await token.connect(holder).confidentialTotalSupplyAccess(); await expect( - fhevm.userDecryptEuint(FhevmType.euint64, totalSupplyHandle, this.token.target, this.holder), + fhevm.userDecryptEuint(FhevmType.euint64, totalSupplyHandle, await token.getAddress(), holder), ).to.eventually.equal(sufficientBalance ? 600 : 1000); }); } it('from zero address', async function () { + const { token, holder } = await deployFixture(); const encryptedInput = await fhevm - .createEncryptedInput(this.token.target, this.holder.address) + .createEncryptedInput(await token.getAddress(), holder.address) .add64(400) .encrypt(); await expect( - this.token - .connect(this.holder) + token + .connect(holder) ['$_burn(address,bytes32,bytes)'](ethers.ZeroAddress, encryptedInput.handles[0], encryptedInput.inputProof), ) - .to.be.revertedWithCustomError(this.token, 'ERC7984InvalidSender') + .to.be.revertedWithCustomError(token, 'ERC7984InvalidSender') .withArgs(ethers.ZeroAddress); }); }); - describe('transfer', function () { - for (const asSender of [true, false]) { - describe(asSender ? 'as sender' : 'as operator', function () { - beforeEach(async function () { - if (!asSender) { - const timestamp = (await ethers.provider.getBlock('latest'))!.timestamp + 100; - await this.token.connect(this.holder).setOperator(this.operator.address, timestamp); - } - }); - - if (!asSender) { - for (const withCallback of [false, true]) { - describe(withCallback ? 'with callback' : 'without callback', function () { - let encryptedInput: any; - let params: any; - - beforeEach(async function () { - encryptedInput = await fhevm - .createEncryptedInput(this.token.target, this.operator.address) - .add64(100) - .encrypt(); - - params = [ - this.holder.address, - this.recipient.address, - encryptedInput.handles[0], - encryptedInput.inputProof, - ]; - if (withCallback) { - params.push('0x'); - } - }); - - it('without operator approval should fail', async function () { - await this.token.$_setOperator(this.holder, this.operator, 0); - - await expect( - this.token - .connect(this.operator) - [ - withCallback - ? 'confidentialTransferFromAndCall(address,address,bytes32,bytes,bytes)' - : 'confidentialTransferFrom(address,address,bytes32,bytes)' - ](...params), - ) - .to.be.revertedWithCustomError(this.token, 'ERC7984UnauthorizedSpender') - .withArgs(this.holder.address, this.operator.address); - }); - - it('should be successful', async function () { - await this.token - .connect(this.operator) - [ - withCallback - ? 'confidentialTransferFromAndCall(address,address,bytes32,bytes,bytes)' - : 'confidentialTransferFrom(address,address,bytes32,bytes)' - ](...params); - }); - }); - } - } - - // Edge cases to run with sender as caller - if (asSender) { - it('with no balance should revert', async function () { - const encryptedInput = await fhevm - .createEncryptedInput(this.token.target, this.recipient.address) - .add64(100) - .encrypt(); - - await expect( - this.token - .connect(this.recipient) - ['confidentialTransfer(address,bytes32,bytes)']( - this.holder.address, - encryptedInput.handles[0], - encryptedInput.inputProof, - ), - ) - .to.be.revertedWithCustomError(this.token, 'ERC7984ZeroBalance') - .withArgs(this.recipient.address); - }); - - it('to zero address', async function () { - const encryptedInput = await fhevm - .createEncryptedInput(this.token.target, this.holder.address) - .add64(100) - .encrypt(); - - await expect( - this.token - .connect(this.holder) - ['confidentialTransfer(address,bytes32,bytes)']( - ethers.ZeroAddress, - encryptedInput.handles[0], - encryptedInput.inputProof, - ), - ) - .to.be.revertedWithCustomError(this.token, 'ERC7984InvalidReceiver') - .withArgs(ethers.ZeroAddress); - }); - } - - for (const sufficientBalance of [false, true]) { - it(`${sufficientBalance ? 'sufficient' : 'insufficient'} balance`, async function () { - const transferAmount = sufficientBalance ? 400 : 1100; - - const encryptedInput = await fhevm - .createEncryptedInput(this.token.target, asSender ? this.holder.address : this.operator.address) - .add64(transferAmount) - .encrypt(); - - let tx; - if (asSender) { - tx = await this.token - .connect(this.holder) - ['confidentialTransfer(address,bytes32,bytes)']( - this.recipient.address, - encryptedInput.handles[0], - encryptedInput.inputProof, - ); - } else { - tx = await this.token - .connect(this.operator) - ['confidentialTransferFrom(address,address,bytes32,bytes)']( - this.holder.address, - this.recipient.address, - encryptedInput.handles[0], - encryptedInput.inputProof, - ); - } - const transferEvent = (await tx.wait()).logs.filter((log: any) => log.address === this.token.target)[0]; - expect(transferEvent.args[0]).to.equal(this.holder.address); - expect(transferEvent.args[1]).to.equal(this.recipient.address); - - const transferAmountHandle = transferEvent.args[2]; - const holderBalanceHandle = await this.token.confidentialBalanceOf(this.holder); - const recipientBalanceHandle = await this.token.confidentialBalanceOf(this.recipient); - - await expect( - fhevm.userDecryptEuint(FhevmType.euint64, transferAmountHandle, this.token.target, this.holder), - ).to.eventually.equal(sufficientBalance ? transferAmount : 0); - await expect( - fhevm.userDecryptEuint(FhevmType.euint64, transferAmountHandle, this.token.target, this.recipient), - ).to.eventually.equal(sufficientBalance ? transferAmount : 0); - // Other can not reencrypt the transfer amount - await expect( - fhevm.userDecryptEuint(FhevmType.euint64, transferAmountHandle, this.token.target, this.operator), - ).to.be.rejectedWith(generateReencryptionErrorMessage(transferAmountHandle, this.operator.address)); - - await expect( - fhevm.userDecryptEuint(FhevmType.euint64, holderBalanceHandle, this.token.target, this.holder), - ).to.eventually.equal(1000 - (sufficientBalance ? transferAmount : 0)); - await expect( - fhevm.userDecryptEuint(FhevmType.euint64, recipientBalanceHandle, this.token.target, this.recipient), - ).to.eventually.equal(sufficientBalance ? transferAmount : 0); - }); - } - }); - } - - describe('without input proof', function () { - for (const [usingTransferFrom, withCallback] of [false, true].flatMap(val => [ - [val, false], - [val, true], - ])) { - describe(`using ${usingTransferFrom ? 'confidentialTransferFrom' : 'confidentialTransfer'} ${ - withCallback ? 'with callback' : '' - }`, function () { - async function callTransfer(contract: any, from: any, to: any, amount: any, sender: any = from) { - let functionParams = [to, amount]; - - if (withCallback) { - functionParams.push('0x'); - if (usingTransferFrom) { - functionParams.unshift(from); - await contract.connect(sender).confidentialTransferFromAndCall(...functionParams); - } else { - await contract.connect(sender).confidentialTransferAndCall(...functionParams); - } - } else { - if (usingTransferFrom) { - functionParams.unshift(from); - await contract.connect(sender).confidentialTransferFrom(...functionParams); - } else { - await contract.connect(sender).confidentialTransfer(...functionParams); - } - } - } - - it('full balance', async function () { - const fullBalanceHandle = await this.token.confidentialBalanceOf(this.holder); - - await callTransfer(this.token, this.holder, this.recipient, fullBalanceHandle); - - await expect( - fhevm.userDecryptEuint( - FhevmType.euint64, - await this.token.confidentialBalanceOf(this.recipient), - this.token.target, - this.recipient, - ), - ).to.eventually.equal(1000); - }); - - it('other user balance should revert', async function () { - const encryptedInput = await fhevm - .createEncryptedInput(this.token.target, this.holder.address) - .add64(100) - .encrypt(); - - await this.token - .connect(this.holder) - ['$_mint(address,bytes32,bytes)'](this.recipient, encryptedInput.handles[0], encryptedInput.inputProof); - - const recipientBalanceHandle = await this.token.confidentialBalanceOf(this.recipient); - await expect(callTransfer(this.token, this.holder, this.recipient, recipientBalanceHandle)) - .to.be.revertedWithCustomError(this.token, 'ERC7984UnauthorizedUseOfEncryptedAmount') - .withArgs(recipientBalanceHandle, this.holder); - }); - - if (usingTransferFrom) { - describe('without operator approval', function () { - beforeEach(async function () { - await this.token.connect(this.holder).setOperator(this.operator.address, 0); - await allowHandle(hre, this.holder, this.operator, await this.token.confidentialBalanceOf(this.holder)); - }); - - it('should revert', async function () { - await expect( - callTransfer( - this.token, - this.holder, - this.recipient, - await this.token.confidentialBalanceOf(this.holder), - this.operator, - ), - ) - .to.be.revertedWithCustomError(this.token, 'ERC7984UnauthorizedSpender') - .withArgs(this.holder.address, this.operator.address); - }); - }); - } - }); - } - }); - - it('internal function reverts on from address zero', async function () { - const encryptedInput = await fhevm - .createEncryptedInput(this.token.target, this.holder.address) - .add64(100) - .encrypt(); - - await expect( - this.token - .connect(this.holder) - ['$_transfer(address,address,bytes32,bytes)']( - ethers.ZeroAddress, - this.recipient.address, - encryptedInput.handles[0], - encryptedInput.inputProof, - ), - ) - .to.be.revertedWithCustomError(this.token, 'ERC7984InvalidSender') - .withArgs(ethers.ZeroAddress); - }); - }); - - describe('transfer with callback', function () { - beforeEach(async function () { - this.recipientContract = await ethers.deployContract('ERC7984ReceiverMock'); - - this.encryptedInput = await fhevm - .createEncryptedInput(this.token.target, this.holder.address) - .add64(1000) - .encrypt(); - }); - - for (const callbackSuccess of [false, true]) { - it(`with callback running ${callbackSuccess ? 'successfully' : 'unsuccessfully'}`, async function () { - const tx = await this.token - .connect(this.holder) - ['confidentialTransferAndCall(address,bytes32,bytes,bytes)']( - this.recipientContract.target, - this.encryptedInput.handles[0], - this.encryptedInput.inputProof, - ethers.AbiCoder.defaultAbiCoder().encode(['bool'], [callbackSuccess]), - ); - - await expect( - fhevm.userDecryptEuint( - FhevmType.euint64, - await this.token.confidentialBalanceOf(this.holder), - this.token.target, - this.holder, - ), - ).to.eventually.equal(callbackSuccess ? 0 : 1000); - - // Verify event contents - expect(tx).to.emit(this.recipientContract, 'ConfidentialTransferCallback').withArgs(callbackSuccess); - const transferEvents = (await tx.wait()).logs.filter((log: any) => log.address === this.token.target); - - const outboundTransferEvent = transferEvents[0]; - const inboundTransferEvent = transferEvents[1]; - - expect(outboundTransferEvent.args[0]).to.equal(this.holder.address); - expect(outboundTransferEvent.args[1]).to.equal(this.recipientContract.target); - await expect( - fhevm.userDecryptEuint(FhevmType.euint64, outboundTransferEvent.args[2], this.token.target, this.holder), - ).to.eventually.equal(1000); - - expect(inboundTransferEvent.args[0]).to.equal(this.recipientContract.target); - expect(inboundTransferEvent.args[1]).to.equal(this.holder.address); - await expect( - fhevm.userDecryptEuint(FhevmType.euint64, inboundTransferEvent.args[2], this.token.target, this.holder), - ).to.eventually.equal(callbackSuccess ? 0 : 1000); - }); - } - - it('with callback reverting without a reason', async function () { - await expect( - this.token - .connect(this.holder) - ['confidentialTransferAndCall(address,bytes32,bytes,bytes)']( - this.recipientContract.target, - this.encryptedInput.handles[0], - this.encryptedInput.inputProof, - '0x', - ), - ) - .to.be.revertedWithCustomError(this.token, 'ERC7984InvalidReceiver') - .withArgs(this.recipientContract.target); - }); - - it('with callback reverting with a custom error', async function () { - await expect( - this.token - .connect(this.holder) - ['confidentialTransferAndCall(address,bytes32,bytes,bytes)']( - this.recipientContract.target, - this.encryptedInput.handles[0], - this.encryptedInput.inputProof, - ethers.AbiCoder.defaultAbiCoder().encode(['uint8'], [2]), - ), - ) - .to.be.revertedWithCustomError(this.recipientContract, 'InvalidInput') - .withArgs(2); - }); - - it('to an EOA', async function () { - await this.token - .connect(this.holder) - ['confidentialTransferAndCall(address,bytes32,bytes,bytes)']( - this.recipient, - this.encryptedInput.handles[0], - this.encryptedInput.inputProof, - '0x', - ); - - const balanceOfHandle = await this.token.confidentialBalanceOf(this.recipient); - await expect( - fhevm.userDecryptEuint(FhevmType.euint64, balanceOfHandle, this.token.target, this.recipient), - ).to.eventually.equal(1000); - }); - }); - describe('disclose', function () { + let [holder, recipient]: HardhatEthersSigner[] = []; + let token: $ERC7984Mock; let expectedAmount: any; let expectedHandle: any; - beforeEach(async function () { + ({ token, holder, recipient } = await deployFixture()); expectedAmount = undefined; expectedHandle = undefined; }); it('user balance', async function () { - const holderBalanceHandle = await this.token.confidentialBalanceOf(this.holder); + const holderBalanceHandle = await token.confidentialBalanceOf(holder); - await this.token.connect(this.holder).discloseEncryptedAmount(holderBalanceHandle); + await token.connect(holder).discloseEncryptedAmount(holderBalanceHandle); expectedAmount = 1000n; expectedHandle = holderBalanceHandle; @@ -536,38 +143,38 @@ describe('ERC7984', function () { it('transaction amount', async function () { const encryptedInput = await fhevm - .createEncryptedInput(this.token.target, this.holder.address) + .createEncryptedInput(await token.getAddress(), holder.address) .add64(400) .encrypt(); - const tx = await this.token['confidentialTransfer(address,bytes32,bytes)']( - this.recipient, + const tx = await token['confidentialTransfer(address,bytes32,bytes)']( + recipient, encryptedInput.handles[0], encryptedInput.inputProof, ); - const transferEvent = (await tx.wait()).logs.filter((log: any) => log.address === this.token.target)[0]; + const transferEvent = (await tx.wait()).logs.filter((log: any) => log.address === token.target)[0]; const transferAmount = transferEvent.args[2]; - await this.token.connect(this.recipient).discloseEncryptedAmount(transferAmount); + await token.connect(recipient).discloseEncryptedAmount(transferAmount); expectedAmount = 400n; expectedHandle = transferAmount; }); it("other user's balance", async function () { - const holderBalanceHandle = await this.token.confidentialBalanceOf(this.holder); + const holderBalanceHandle = await token.confidentialBalanceOf(holder); - await expect(this.token.connect(this.recipient).discloseEncryptedAmount(holderBalanceHandle)) - .to.be.revertedWithCustomError(this.token, 'ERC7984UnauthorizedUseOfEncryptedAmount') - .withArgs(holderBalanceHandle, this.recipient); + await expect(token.connect(recipient).discloseEncryptedAmount(holderBalanceHandle)) + .to.be.revertedWithCustomError(token, 'ERC7984UnauthorizedUseOfEncryptedAmount') + .withArgs(holderBalanceHandle, recipient); }); it('invalid signature reverts', async function () { - const holderBalanceHandle = await this.token.confidentialBalanceOf(this.holder); - await this.token.connect(this.holder).discloseEncryptedAmount(holderBalanceHandle); + const holderBalanceHandle = await token.confidentialBalanceOf(holder); + await token.connect(holder).discloseEncryptedAmount(holderBalanceHandle); - await expect(this.token.connect(this.holder).finalizeDiscloseEncryptedAmount(0, 0, [])).to.be.reverted; + await expect(token.connect(holder).finalizeDiscloseEncryptedAmount(0, 0, [])).to.be.reverted; }); afterEach(async function () { @@ -576,15 +183,14 @@ describe('ERC7984', function () { await fhevm.awaitDecryptionOracle(); // Check that event was correctly emitted - const eventFilter = this.token.filters.AmountDisclosed(); - const discloseEvent = (await this.token.queryFilter(eventFilter))[0]; + const eventFilter = token.filters.AmountDisclosed(); + const discloseEvent = (await token.queryFilter(eventFilter))[0]; expect(discloseEvent.args[0]).to.equal(expectedHandle); expect(discloseEvent.args[1]).to.equal(expectedAmount); }); }); + + shouldBehaveLikeERC7984(contract); }); -/* eslint-enable no-unexpected-multiline */ -function generateReencryptionErrorMessage(handle: string, account: string): string { - return `User ${account} is not authorized to user decrypt handle ${handle}`; -} +export { deployFixture as deployERC7984Fixture }; diff --git a/test/token/ERC7984/ERC7984Votes.test.ts b/test/token/ERC7984/ERC7984Votes.test.ts index 3e5be5de..913a44a5 100644 --- a/test/token/ERC7984/ERC7984Votes.test.ts +++ b/test/token/ERC7984/ERC7984Votes.test.ts @@ -261,6 +261,7 @@ describe('ERC7984Votes', function () { // Check total supply for each block const afterFirstMintSupplyHandle = await this.token.getPastTotalSupply(afterFirstMintBlock); + await this.token.getPastTotalSupplyAccess(afterFirstMintBlock); await expect( fhevm.userDecryptEuint(FhevmType.euint64, afterFirstMintSupplyHandle, this.token.target, this.holder), ).to.eventually.equal(1000); @@ -268,6 +269,7 @@ describe('ERC7984Votes', function () { await expect(this.token.getPastTotalSupply(afterTransferBlock)).to.eventually.eq(afterFirstMintSupplyHandle); const afterSecondMintSupplyHandle = await this.token.getPastTotalSupply(afterSecondMintBlock); + await this.token.getPastTotalSupplyAccess(afterSecondMintBlock); await expect( fhevm.userDecryptEuint(FhevmType.euint64, afterSecondMintSupplyHandle, this.token.target, this.holder), ).to.eventually.equal(2000); diff --git a/test/token/ERC7984/extensions/ERC7984Freezable.test.ts b/test/token/ERC7984/extensions/ERC7984Freezable.test.ts index 6bef4f42..38528198 100644 --- a/test/token/ERC7984/extensions/ERC7984Freezable.test.ts +++ b/test/token/ERC7984/extensions/ERC7984Freezable.test.ts @@ -1,11 +1,13 @@ import { IACL__factory } from '../../../../types'; import { $ERC7984FreezableMock } from '../../../../types/contracts-exposed/mocks/token/ERC7984FreezableMock.sol/$ERC7984FreezableMock'; import { ACL_ADDRESS } from '../../../helpers/accounts'; +import { shouldBehaveLikeERC7984 } from '../ERC7984.behaviour'; import { FhevmType } from '@fhevm/hardhat-plugin'; import { expect } from 'chai'; import { AddressLike, BytesLike, EventLog } from 'ethers'; import { ethers, fhevm } from 'hardhat'; +const contract = '$ERC7984FreezableMock'; const name = 'ConfidentialFungibleToken'; const symbol = 'CFT'; const uri = 'https://example.com/metadata'; @@ -13,12 +15,19 @@ const uri = 'https://example.com/metadata'; describe('ERC7984Freezable', function () { async function deployFixture() { const [holder, recipient, freezer, operator, anyone] = await ethers.getSigners(); - const token = (await ethers.deployContract('$ERC7984FreezableMock', [ + const token = (await ethers.deployContract(contract, [ name, symbol, uri, freezer.address, ])) as any as $ERC7984FreezableMock; + const encryptedInput = await fhevm + .createEncryptedInput(await token.getAddress(), holder.address) + .add64(1000) + .encrypt(); + await token + .connect(holder) + ['$_mint(address,bytes32,bytes)'](holder, encryptedInput.handles[0], encryptedInput.inputProof); const acl = IACL__factory.connect(ACL_ADDRESS, ethers.provider); return { token, acl, holder, recipient, freezer, operator, anyone }; } @@ -229,4 +238,9 @@ describe('ERC7984Freezable', function () { .to.be.revertedWithCustomError(token, 'ERC7984UnauthorizedUseOfEncryptedAmount') .withArgs(encryptedInput.handles[0], anyone); }); + + shouldBehaveLikeERC7984( + contract, + '0x0000000000000000000000000000000000000001', // freezer + ); });