diff --git a/.changeset/stupid-dogs-bow.md b/.changeset/stupid-dogs-bow.md new file mode 100644 index 00000000..2eda7adb --- /dev/null +++ b/.changeset/stupid-dogs-bow.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-confidential-contracts': minor +--- + +`ConfidentialFungibleTokenERC20Events`: Extension of `ConfidentialFungibleToken` that emits ERC20 events on transfers. diff --git a/contracts/mocks/ConfidentialFungibleTokenERC20EventsMock.sol b/contracts/mocks/ConfidentialFungibleTokenERC20EventsMock.sol new file mode 100644 index 00000000..f10097f8 --- /dev/null +++ b/contracts/mocks/ConfidentialFungibleTokenERC20EventsMock.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {SepoliaConfig} from "@fhevm/solidity/config/ZamaConfig.sol"; +import {FHE, euint64, externalEuint64} from "@fhevm/solidity/lib/FHE.sol"; +import {ConfidentialFungibleTokenERC20Events} from "./../token/extensions/ConfidentialFungibleTokenERC20Events.sol"; + +// solhint-disable func-name-mixedcase +abstract contract ConfidentialFungibleTokenERC20EventsMock is ConfidentialFungibleTokenERC20Events, SepoliaConfig { + function $_mint( + address to, + externalEuint64 encryptedAmount, + bytes calldata inputProof + ) public returns (euint64 transferred) { + return _mint(to, FHE.fromExternal(encryptedAmount, inputProof)); + } +} diff --git a/contracts/token/README.adoc b/contracts/token/README.adoc index 88f9e110..1dd1a11a 100644 --- a/contracts/token/README.adoc +++ b/contracts/token/README.adoc @@ -7,6 +7,7 @@ This set of interfaces, contracts, and utilities are all related to the evolving - {ConfidentialFungibleToken}: Implementation of {IConfidentialFungibleToken}. - {ConfidentialFungibleTokenERC20Wrapper}: Extension of {ConfidentialFungibleToken} which wraps an `ERC20` into a confidential token. The wrapper allows for free conversion in both directions at a fixed rate. +- {ConfidentialFungibleTokenERC20Events}: Extension of {ConfidentialFungibleToken} that emits ERC20 events on transfers. - {ConfidentialFungibleTokenUtils}: A library that provides the on-transfer callback check used by {ConfidentialFungibleToken}. == Core @@ -14,6 +15,7 @@ This set of interfaces, contracts, and utilities are all related to the evolving == Extensions {{ConfidentialFungibleTokenERC20Wrapper}} +{{ConfidentialFungibleTokenERC20Events}} == Utilities {{ConfidentialFungibleTokenUtils}} \ No newline at end of file diff --git a/contracts/token/extensions/ConfidentialFungibleTokenERC20Events.sol b/contracts/token/extensions/ConfidentialFungibleTokenERC20Events.sol new file mode 100644 index 00000000..2ee85be3 --- /dev/null +++ b/contracts/token/extensions/ConfidentialFungibleTokenERC20Events.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ConfidentialFungibleToken, euint64} from "../ConfidentialFungibleToken.sol"; + +/** + * @dev Extension of `ConfidentialFungibleToken` that emits ERC20 events on transfers. This + * can be useful for surfacing confidential transfers on applications that support ERC20 events such as Etherscan. + * + * NOTE: The ERC20 events emitted only have meaningful data for the `to` and `from` fields. The `amount` field + * is fixed to 1. + */ +abstract contract ConfidentialFungibleTokenERC20Events is ConfidentialFungibleToken { + function _update(address from, address to, euint64 amount) internal virtual override returns (euint64) { + emit IERC20.Transfer(from, to, 1); + return super._update(from, to, amount); + } +} diff --git a/test/token/extensions/ConfidentialFungibleTokenERC20Events.test.ts b/test/token/extensions/ConfidentialFungibleTokenERC20Events.test.ts new file mode 100644 index 00000000..f748a493 --- /dev/null +++ b/test/token/extensions/ConfidentialFungibleTokenERC20Events.test.ts @@ -0,0 +1,31 @@ +import { expect } from 'chai'; +import { ethers, fhevm } from 'hardhat'; + +const name = 'ConfidentialFungibleToken'; +const symbol = 'CFT'; +const uri = 'https://example.com/metadata'; + +describe('ConfidentialFungibleTokenERC20Events', function () { + beforeEach(async function () { + const [holder] = await ethers.getSigners(); + + const token = await ethers.deployContract('$ConfidentialFungibleTokenERC20EventsMock', [name, symbol, uri]); + this.token = token; + this.holder = holder; + }); + + it('should emit ERC20 transfer event', async function () { + const encryptedInput = await fhevm + .createEncryptedInput(this.token.target, this.holder.address) + .add64(100) + .encrypt(); + + await expect( + this.token + .connect(this.holder) + ['$_mint(address,bytes32,bytes)'](this.holder, encryptedInput.handles[0], encryptedInput.inputProof), + ) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.holder.address, 1); + }); +});