From e846a172c8999b2e12aa7b4eb397717ce130bc30 Mon Sep 17 00:00:00 2001 From: halftone-dev Date: Thu, 26 Sep 2024 15:08:44 +0100 Subject: [PATCH 1/2] Draft FHERC20: Events now emitted. Encrypted functions renamed to `enc{ERC20FunctionName}`. Encrypted variables renamed to `e{Name}`. `inExxx` renamed to `ie{Name}`. `wrap` / `unwrap` renamed to `encrypt` / `decrypt`. Broke encrypted public variables into `enc___` version and `sealed___` version. Added `_encBeforeTokenTransfer` and `_encAfterTokenTransfer` hooks. Cleaned up imports, events, and errors. --- contracts/draft/FHERC20.sol | 340 +++++++++++++++++++++++++++++++++++ contracts/draft/IFHERC20.sol | 158 ++++++++++++++++ 2 files changed, 498 insertions(+) create mode 100644 contracts/draft/FHERC20.sol create mode 100644 contracts/draft/IFHERC20.sol diff --git a/contracts/draft/FHERC20.sol b/contracts/draft/FHERC20.sol new file mode 100644 index 0000000..c6880a4 --- /dev/null +++ b/contracts/draft/FHERC20.sol @@ -0,0 +1,340 @@ +// solhint-disable no-empty-blocks +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.19 <0.9.0; + +import {FHE, euint128, inEuint128} from "../FHE.sol"; +import {PermissionedV2, PermissionV2} from "./PermissionedV2.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {IFHERC20} from "./IFHERC20.sol"; + +contract FHERC20 is IFHERC20, ERC20, PermissionedV2 { + // A mapping from address to an encrypted balance. + mapping(address => euint128) internal _encBalances; + // A mapping from address (owner) to a mapping of address (spender) to an encrypted amount. + mapping(address => mapping(address => euint128)) internal _encAllowances; + euint128 internal _encTotalSupply = FHE.asEuint128(0); + + constructor( + string memory name, + string memory symbol, + address permitV2 // TODO: Remove when PermitV2 deployed to fixed address + ) ERC20(name, symbol) PermissionedV2(permitV2, "FHERC20") {} + + /** + * @dev Returns the encrypted value of tokens in existence. + */ + function encTotalSupply() public view virtual override returns (euint128) { + return _encTotalSupply; + } + + /** + * @dev Returns the encrypted value of tokens in existence, sealed for the caller. + */ + function sealedTotalSupply( + PermissionV2 calldata permission + ) + public + view + virtual + override + withPermission(permission) + returns (string memory) + { + return _encTotalSupply.seal(permission.publicKey); + } + + /** + * @dev Returns the value of the encrypted tokens owned by `account` + */ + function encBalanceOf( + address account + ) public view virtual override returns (euint128) { + return _encBalances[account]; + } + + /** + * @dev Returns the value of the encrypted tokens owned by the issuer of the PermitNft, sealed for the caller + */ + function sealedBalanceOf( + PermissionV2 calldata permission + ) + public + view + virtual + override + withPermission(permission) + returns (string memory) + { + return _encBalances[permission.issuer].seal(permission.publicKey); + } + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to`. + * Accepts the value as inEuint128, more convenient for calls from EOAs. + * + * Returns a boolean value indicating whether the operation succeeded. + */ + function encTransfer( + address to, + inEuint128 calldata ieAmount + ) public virtual override returns (bool) { + _encTransfer(msg.sender, to, FHE.asEuint128(ieAmount)); + return true; + } + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function encAllowance( + address owner, + address spender + ) public view virtual override returns (euint128) { + return _encAllowances[owner][spender]; + } + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. Sealed for the caller. + * + * Permission issuer must be either the owner or spender. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function sealedAllowance( + PermissionV2 calldata permission, + address owner, + address spender + ) + public + view + virtual + override + withPermission(permission) + returns (string memory) + { + if (permission.issuer != owner && permission.issuer != spender) { + revert FHERC20NotOwnerOrSpender(); + } + return _encAllowances[owner][spender].seal(permission.publicKey); + } + + /** + * @dev Sets `ieAmount` tokens as the allowance of `spender` over the + * caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits an {EncApproved} event. + */ + function encApprove( + address spender, + inEuint128 calldata ieAmount + ) public virtual override returns (bool) { + _encApprove(msg.sender, spender, FHE.asEuint128(ieAmount)); + return true; + } + + /** + * @dev Moves `ieAmount` tokens from `from` to `to` using the + * allowance mechanism. `value` is then deducted from the caller's + * allowance. Accepts the value as inEuint128, more convenient for calls from EOAs. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {EncTransfer} event. + */ + function encTransferFrom( + address from, + address to, + inEuint128 calldata ieAmount + ) public virtual override returns (bool) { + euint128 encSpent = _encSpendAllowance( + from, + msg.sender, + FHE.asEuint128(ieAmount) + ); + _encTransfer(from, to, encSpent); + return true; + } + + /** + * @dev Encrypts `amount` tokens, reducing the callers public balance by `amount`, + * and increasing their `encBalance` by `amount`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits an {Encrypted} event. + */ + function encrypt(uint128 amount) public virtual override returns (bool) { + _burn(msg.sender, amount); + _encMint(msg.sender, FHE.asEuint128(amount)); + + emit Encrypted(msg.sender, amount); + + return true; + } + + /** + * @dev Decrypts `amount` tokens, reducing the callers `encBalance` by `amount`, + * and increasing their public balance by `amount`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Decrypted} event. + */ + function decrypt(uint128 amount) public virtual override returns (bool) { + euint128 eAmount = _encBurn(msg.sender, FHE.asEuint128(amount)); + amount = FHE.decrypt(eAmount); + _mint(msg.sender, amount); + + emit Decrypted(msg.sender, amount); + + return true; + } + + /** + * @dev Moves `eAmount` tokens from the caller's account to `to`. + * Accepts the value as euint128, more convenient for calls from other contracts + * + * Returns an `euint128` of the true amount transferred. + * + * Emits an {EncTransfer} event. + */ + function _encTransfer( + address from, + address to, + euint128 eAmount + ) internal returns (euint128) { + if (from == address(0)) { + revert ERC20InvalidSender(address(0)); + } + if (to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + + // Make sure the sender has enough tokens. + eAmount = FHE.select( + eAmount.lte(_encBalances[from]), + eAmount, + FHE.asEuint128(0) + ); + + _encBeforeTokenTransfer(from, to, eAmount); + + // Add to the balance of `to` and subtract from the balance of `from`. + _encBalances[to] = _encBalances[to] + eAmount; + _encBalances[from] = _encBalances[from] - eAmount; + + emit EncTransfer(from, to); + + _encAfterTokenTransfer(from, to, eAmount); + + return eAmount; + } + + /** + * @dev Creates `eAmount` encrypted tokens and assigns them to `to`. + * Increases `encTotalSupply` by `eAmount` + * Accepts the value as euint128, more convenient for calls from other contracts + * + * Emits an {EncTransfer} event with `from` set to the zero address. + */ + function _encMint(address to, euint128 eAmount) internal { + if (account == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + + _encBeforeTokenTransfer(address(0), to, eAmount); + + _encBalances[to] = _encBalances[to] + eAmount; + _encTotalSupply = _encTotalSupply + eAmount; + + emit EncTransfer(address(0), to); + + _encAfterTokenTransfer(address(0), to, eAmount); + } + + /** + * @dev Destroys `eAmount` encrypted tokens from `to`. + * Decreases `encTotalSupply` by `eAmount` + * Accepts the value as euint128, more convenient for calls from other contracts + * + * Emits an {EncTransfer} event with `to` set to the zero address. + */ + function _encBurn( + address from, + euint128 eAmount + ) internal returns (euint128) { + if (account == address(0)) { + revert ERC20InvalidSender(address(0)); + } + + eAmount = FHE.select( + _encBalances[msg.sender].gte(eAmount), + eAmount, + FHE.asEuint128(0) + ); + + _encBeforeTokenTransfer(from, address(0), eAmount); + + _encBalances[from] = _encBalances[from] - eAmount; + _encTotalSupply = _encTotalSupply - eAmount; + + emit EncTransfer(from, address(0)); + + _encAfterTokenTransfer(from, address(0), eAmount); + + return eAmount; + } + + function _encApprove( + address owner, + address spender, + euint128 eAmount + ) internal { + if (owner == address(0)) { + revert ERC20InvalidApprover(address(0)); + } + if (spender == address(0)) { + revert ERC20InvalidSpender(address(0)); + } + _encAllowances[owner][spender] = eAmount; + } + + function _encSpendAllowance( + address owner, + address spender, + euint128 eAmount + ) internal virtual returns (euint128) { + euint128 eCurrentAllowance = _encAllowances[owner][spender]; + euint128 eSpent = FHE.min(eCurrentAllowance, eAmount); + _encApprove(owner, spender, (eCurrentAllowance - eSpent)); + + return eSpent; + } + + /** + * @dev Hook that is called before any transfer of encrypted tokens. This includes + * minting and burning. + */ + function _encBeforeTokenTransfer( + address from, + address to, + euint128 eAmount + ) internal virtual {} + + /** + * @dev Hook that is called after any transfer of encrypted tokens. This includes + * minting and burning. + */ + function _encAfterTokenTransfer( + address from, + address to, + euint128 eAmount + ) internal virtual {} +} diff --git a/contracts/draft/IFHERC20.sol b/contracts/draft/IFHERC20.sol new file mode 100644 index 0000000..1ed406b --- /dev/null +++ b/contracts/draft/IFHERC20.sol @@ -0,0 +1,158 @@ +pragma solidity >=0.8.19 <0.9.0; + +// SPDX-License-Identifier: MIT +// Fhenix Protocol (last updated v0.1.0) (token/FHERC20/IFHERC20.sol) +// Inspired by OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts) (token/ERC20/IERC20.sol) + +import {euint128, inEuint128} from "../FHE.sol"; +import {PermissionV2} from "./PermissionedV2.sol"; + +/** + * @dev Interface of the ERC-20 standard as defined in the ERC. + */ +interface IFHERC20 { + error FHERC20NotOwnerOrSpender(); + + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event EncTransfer(address indexed from, address indexed to); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approveEncrypted}. `value` is the new allowance. + */ + event EncApproved(address indexed owner, address indexed spender); + + /** + * @dev Emitted when `amount` tokens are transferred from `_balance[owner]` to `_encBalance[owner]` + * by a call to {encrypt}. + */ + event Encrypted(address indexed owner, uint256 amount); + + /** + * @dev Emitted when `amount` tokens are transferred from `_encBalance[owner]` to `_balance[owner]` + * by a call to {decrypt}. + */ + event Decrypted(address indexed owner, uint256 amount); + + /** + * @dev Returns the encrypted value of tokens in existence. + */ + function encTotalSupply() external view returns (euint128); + + /** + * @dev Returns the encrypted value of tokens in existence, sealed for the caller. + */ + function sealedTotalSupply( + PermissionV2 calldata permission + ) external view returns (string memory); + + /** + * @dev Returns the value of the encrypted tokens owned by `account` + */ + function encBalanceOf(address account) external view returns (euint128); + + /** + * @dev Returns the value of the encrypted tokens owned by the issuer of the PermitNft, sealed for the caller + */ + function sealedBalanceOf( + PermissionV2 memory permission + ) external view returns (string memory); + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to`. + * Accepts the value as inEuint128, more convenient for calls from EOAs. + * + * Returns a boolean value indicating whether the operation succeeded. + */ + function encTransfer( + address to, + inEuint128 calldata ieAmount + ) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function encAllowance( + address owner, + address spender + ) external view returns (euint128); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. Sealed for the caller. + * + * Permission issuer must be either the owner or spender. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function sealedAllowance( + PermissionV2 memory permission, + address owner, + address spender + ) external view returns (string memory); + + /** + * @dev Sets `ieAmount` tokens as the allowance of `spender` over the + * caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {ApprovalEncrypted} event. + */ + function encApprove( + address spender, + inEuint128 calldata ieAmount + ) external returns (bool); + + /** + * @dev Moves `ieAmount` tokens from `from` to `to` using the + * allowance mechanism. `value` is then deducted from the caller's + * allowance. Accepts the value as inEuint128, more convenient for calls from EOAs. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {TransferEncrypted} event. + */ + function encTransferFrom( + address from, + address to, + inEuint128 calldata ieAmount + ) external returns (bool); + + /** + * @dev Encrypts `amount` tokens, reducing the callers public balance by `amount`, + * and increasing their `encBalance` by `amount`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Encrypted} event. + */ + function encrypt(uint128 amount) external returns (bool); + + /** + * @dev Decrypts `amount` tokens, reducing the callers `encBalance` by `amount`, + * and increasing their public balance by `amount`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Decrypted} event. + */ + function decrypt(uint128 amount) external returns (bool); +} From 6789d3cbcaaa37675db59ece9fd3def00e3ca6fe Mon Sep 17 00:00:00 2001 From: halftone-dev Date: Wed, 30 Oct 2024 11:57:50 +0000 Subject: [PATCH 2/2] Functions exposing enc variables gated behind `permitId` and `withPermitRouter` --- contracts/draft/FHERC20.sol | 32 +++++++++++++++++++----------- contracts/draft/IFHERC20.sol | 17 ++++++++++++---- contracts/draft/PermissionedV2.sol | 10 +++++----- 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/contracts/draft/FHERC20.sol b/contracts/draft/FHERC20.sol index c6880a4..f851694 100644 --- a/contracts/draft/FHERC20.sol +++ b/contracts/draft/FHERC20.sol @@ -22,8 +22,11 @@ contract FHERC20 is IFHERC20, ERC20, PermissionedV2 { /** * @dev Returns the encrypted value of tokens in existence. + * + * @dev Designed to be used as part of a write tx + * @dev Can only be called by an authorized router (see PermitV2 documentation) */ - function encTotalSupply() public view virtual override returns (euint128) { + function encTotalSupply(uint256 _permitId) public virtual override withPermitRouter(_permitId) returns (euint128) { return _encTotalSupply; } @@ -40,16 +43,18 @@ contract FHERC20 is IFHERC20, ERC20, PermissionedV2 { withPermission(permission) returns (string memory) { - return _encTotalSupply.seal(permission.publicKey); + return _encTotalSupply.seal(permission.sealingKey); } /** * @dev Returns the value of the encrypted tokens owned by `account` + * @dev Designed to be used as part of a write tx + * @dev Can only be called by an authorized router (see PermitV2 documentation) */ function encBalanceOf( - address account - ) public view virtual override returns (euint128) { - return _encBalances[account]; + uint256 permitId + ) public virtual override withPermitRouter(permitId) returns (euint128) { + return _encBalances[permitIssuer]; } /** @@ -65,7 +70,7 @@ contract FHERC20 is IFHERC20, ERC20, PermissionedV2 { withPermission(permission) returns (string memory) { - return _encBalances[permission.issuer].seal(permission.publicKey); + return _encBalances[permission.issuer].seal(permission.sealingKey); } /** @@ -88,12 +93,15 @@ contract FHERC20 is IFHERC20, ERC20, PermissionedV2 { * zero by default. * * This value changes when {approve} or {transferFrom} are called. + * + * @dev Designed to be used as part of a write tx + * @dev Can only be called by an authorized router (see PermitV2 documentation) */ function encAllowance( - address owner, + uint256 permitId, address spender - ) public view virtual override returns (euint128) { - return _encAllowances[owner][spender]; + ) public virtual override withPermitRouter(permitId) returns (euint128) { + return _encAllowances[permitIssuer][spender]; } /** @@ -120,7 +128,7 @@ contract FHERC20 is IFHERC20, ERC20, PermissionedV2 { if (permission.issuer != owner && permission.issuer != spender) { revert FHERC20NotOwnerOrSpender(); } - return _encAllowances[owner][spender].seal(permission.publicKey); + return _encAllowances[owner][spender].seal(permission.sealingKey); } /** @@ -245,7 +253,7 @@ contract FHERC20 is IFHERC20, ERC20, PermissionedV2 { * Emits an {EncTransfer} event with `from` set to the zero address. */ function _encMint(address to, euint128 eAmount) internal { - if (account == address(0)) { + if (to == address(0)) { revert ERC20InvalidReceiver(address(0)); } @@ -270,7 +278,7 @@ contract FHERC20 is IFHERC20, ERC20, PermissionedV2 { address from, euint128 eAmount ) internal returns (euint128) { - if (account == address(0)) { + if (from == address(0)) { revert ERC20InvalidSender(address(0)); } diff --git a/contracts/draft/IFHERC20.sol b/contracts/draft/IFHERC20.sol index 1ed406b..7e9dc97 100644 --- a/contracts/draft/IFHERC20.sol +++ b/contracts/draft/IFHERC20.sol @@ -41,8 +41,11 @@ interface IFHERC20 { /** * @dev Returns the encrypted value of tokens in existence. + * + * @dev Designed to be used as part of a write tx + * @dev Can only be called by an authorized router (see PermitV2 documentation) */ - function encTotalSupply() external view returns (euint128); + function encTotalSupply(uint256 permitId) external returns (euint128); /** * @dev Returns the encrypted value of tokens in existence, sealed for the caller. @@ -53,8 +56,11 @@ interface IFHERC20 { /** * @dev Returns the value of the encrypted tokens owned by `account` + * + * @dev Designed to be used as part of a write tx + * @dev Can only be called by an authorized router (see PermitV2 documentation) */ - function encBalanceOf(address account) external view returns (euint128); + function encBalanceOf(uint256 permitId) external returns (euint128); /** * @dev Returns the value of the encrypted tokens owned by the issuer of the PermitNft, sealed for the caller @@ -80,11 +86,14 @@ interface IFHERC20 { * zero by default. * * This value changes when {approve} or {transferFrom} are called. + * + * @dev Designed to be used as part of a write tx + * @dev Can only be called by an authorized router (see PermitV2 documentation) */ function encAllowance( - address owner, + uint256 permitId, address spender - ) external view returns (euint128); + ) external returns (euint128); /** * @dev Returns the remaining number of tokens that `spender` will be diff --git a/contracts/draft/PermissionedV2.sol b/contracts/draft/PermissionedV2.sol index 3d47c36..c9756b1 100644 --- a/contracts/draft/PermissionedV2.sol +++ b/contracts/draft/PermissionedV2.sol @@ -46,16 +46,16 @@ abstract contract PermissionedV2 is IFhenixPermissionedV2 { } /// @dev Populated in txs when `withPermitRouter` is added as a fn modifier */ - address issuer = address(0); + address permitIssuer = address(0); /// @dev Ensures that the msg.sender is a router and user has granted router permissions - /// @dev Populates `issuer` with the owner of the permit + /// @dev Populates `permitIssuer` with the owner of the permit /// - /// @dev ** Only return the data of `issuer`! ** + /// @dev ** Only return the data of `permitIssuer`! ** modifier withPermitRouter(uint256 _permitId) { - issuer = PERMIT_V2.validatePermitRouter(_permitId, msg.sender); + permitIssuer = PERMIT_V2.validatePermitRouter(_permitId, msg.sender); _; - issuer = address(0); + permitIssuer = address(0); } // Utility functions to enable PermitV2 contract to be fully abstracted away