Skip to content

Commit

Permalink
Merge pull request #64 from PacificYield/erc20-wrapper
Browse files Browse the repository at this point in the history
feat: ConfidentialERC20Wrapped/ConfidentialWETH
  • Loading branch information
PacificYield authored Dec 23, 2024
2 parents 500d185 + 8d652e8 commit 4917e07
Show file tree
Hide file tree
Showing 13 changed files with 1,062 additions and 3 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,19 @@ contract MyERC20 is SepoliaZamaFHEVMConfig, ConfidentialERC20 {

These Solidity templates include governance-related and token-related contracts.

### Finance

- [ConfidentialVestingWallet](./contracts/token/finance/ConfidentialVestingWallet.sol)
- [ConfidentialVestingWalletCliff](./contracts/token/finance/ConfidentialVestingWalletCliff.sol)

### Token

- [ConfidentialERC20](./contracts/token/ERC20/ConfidentialERC20.sol)
- [ConfidentialERC20Mintable](./contracts/token/ERC20/extensions/ConfidentialERC20Mintable.sol)
- [ConfidentialERC20WithErrors](./contracts/token/ERC20/extensions/ConfidentialERC20WithErrors.sol)
- [ConfidentialERC20WithErrorsMintable](./contracts/token/ERC20/extensions/ConfidentialERC20WithErrorsMintable.sol)
- [ConfidentialERC20Wrapped](./contracts/token/ERC20/ConfidentialERC20Wrapped.sol)
- [ConfidentialWETH](./contracts/token/ERC20/ConfidentialWETH.sol)

### Governance

Expand All @@ -82,7 +89,7 @@ These Solidity templates include governance-related and token-related contracts.

- [EncryptedErrors](./contracts/utils/EncryptedErrors.sol)

### Contributing
## Contributing

There are two ways to contribute to the Zama fhEVM contracts:

Expand Down
10 changes: 10 additions & 0 deletions contracts/test/token/ERC20/TestConfidentialERC20Wrapped.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.24;

import { ConfidentialERC20Wrapped } from "../../../token/ERC20/ConfidentialERC20Wrapped.sol";
import { SepoliaZamaFHEVMConfig } from "fhevm/config/ZamaFHEVMConfig.sol";
import { SepoliaZamaGatewayConfig } from "fhevm/config/ZamaGatewayConfig.sol";

contract TestConfidentialERC20Wrapped is SepoliaZamaFHEVMConfig, SepoliaZamaGatewayConfig, ConfidentialERC20Wrapped {
constructor(address erc20_, uint256 maxDecryptionDelay_) ConfidentialERC20Wrapped(erc20_, maxDecryptionDelay_) {}
}
10 changes: 10 additions & 0 deletions contracts/test/token/ERC20/TestConfidentialWETH.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.24;

import { ConfidentialWETH } from "../../../token/ERC20/ConfidentialWETH.sol";
import { SepoliaZamaFHEVMConfig } from "fhevm/config/ZamaFHEVMConfig.sol";
import { SepoliaZamaGatewayConfig } from "fhevm/config/ZamaGatewayConfig.sol";

contract TestConfidentialWETH is SepoliaZamaFHEVMConfig, SepoliaZamaGatewayConfig, ConfidentialWETH {
constructor(uint256 maxDecryptionDelay_) ConfidentialWETH(maxDecryptionDelay_) {}
}
38 changes: 38 additions & 0 deletions contracts/test/token/ERC20/TestERC20Mintable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.24;

import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { Ownable2Step, Ownable } from "@openzeppelin/contracts/access/Ownable2Step.sol";

/**
* @title ERC20Mintable
* @notice This contract is an ERC20 token that is mintable by the owner.
*/
contract ERC20Mintable is ERC20, Ownable2Step {
/// @dev override number of decimals
uint8 private immutable _DECIMALS;

constructor(
string memory name_,
string memory symbol_,
uint8 decimals_,
address owner_
) ERC20(name_, symbol_) Ownable(owner_) {
_DECIMALS = decimals_;
}

/**
* @notice Returns the number of decimals.
*/
function decimals() public view override returns (uint8) {
return _DECIMALS;
}

/**
* @notice Mint tokens.
* @param amount Amount of tokens to mint.
*/
function mint(uint256 amount) public onlyOwner {
_mint(msg.sender, amount);
}
}
168 changes: 168 additions & 0 deletions contracts/token/ERC20/ConfidentialERC20Wrapped.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.24;

import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { ReentrancyGuardTransient } from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";

import "fhevm/lib/TFHE.sol";
import "fhevm/gateway/GatewayCaller.sol";

import { IConfidentialERC20Wrapped } from "./IConfidentialERC20Wrapped.sol";
import { ConfidentialERC20 } from "./ConfidentialERC20.sol";

/**
* @title ConfidentialERC20Wrapped
* @notice This contract allows users to wrap/unwrap trustlessly
* ERC20 tokens to ConfidentialERC20 tokens.
* @dev This implementation does not support tokens with rebase functions or
* tokens with a fee on transfer. All ERC20 tokens must have decimals
* inferior or equal to 18 decimals but superior or equal to 6 decimals.
*/
abstract contract ConfidentialERC20Wrapped is
ConfidentialERC20,
IConfidentialERC20Wrapped,
ReentrancyGuardTransient,
GatewayCaller
{
using SafeERC20 for IERC20Metadata;

/// @notice Returned if the maximum decryption delay is higher than 1 day.
error MaxDecryptionDelayTooHigh();

/// @notice ERC20 token that is wrapped.
IERC20Metadata public immutable ERC20_TOKEN;

/// @notice Tracks whether the account can move funds.
mapping(address account => bool isRestricted) public isAccountRestricted;

/// @notice Tracks the unwrap request to a unique request id.
mapping(uint256 requestId => UnwrapRequest unwrapRequest) public unwrapRequests;

/**
* @notice Deposit/withdraw ERC20 tokens using confidential ERC20 tokens.
* @param erc20_ Address of the ERC20 token to wrap/unwrap.
* @dev The name/symbol are autogenerated.
* For instance,
* "Wrapped Ether" --> "Confidential Wrapped Ether"
* "WETH" --> "WETHc".
* @param maxDecryptionDelay_ Maximum delay for the Gateway to decrypt.
* @dev Do not use a small value in production to avoid security issues if the response
* cannot be processed because the block time is higher than the delay.
* The current implementation expects the Gateway to always return a decrypted
* value within the delay specified, as long as it is sufficient enough.
*/
constructor(
address erc20_,
uint256 maxDecryptionDelay_
)
ConfidentialERC20(
string(abi.encodePacked("Confidential ", IERC20Metadata(erc20_).name())),
string(abi.encodePacked(IERC20Metadata(erc20_).symbol(), "c"))
)
{
ERC20_TOKEN = IERC20Metadata(erc20_);

/// @dev The maximum delay is set to 1 day.
if (maxDecryptionDelay_ > 1 days) {
revert MaxDecryptionDelayTooHigh();
}
}

/**
* @notice Unwrap ConfidentialERC20 tokens to standard ERC20 tokens.
* @param amount Amount to unwrap.
*/
function unwrap(uint64 amount) public virtual {
_canTransferOrUnwrap(msg.sender);

/// @dev Once this function is called, it becomes impossible for the sender to move any token.
isAccountRestricted[msg.sender] = true;
ebool canUnwrap = TFHE.le(amount, _balances[msg.sender]);

uint256[] memory cts = new uint256[](1);
cts[0] = Gateway.toUint256(canUnwrap);

uint256 requestId = Gateway.requestDecryption(
cts,
this.callbackUnwrap.selector,
0,
block.timestamp + 100,
false
);

unwrapRequests[requestId] = UnwrapRequest({ account: msg.sender, amount: amount });
}

/**
* @notice Wrap ERC20 tokens to an encrypted format.
* @param amount Amount to wrap.
*/
function wrap(uint256 amount) public virtual {
ERC20_TOKEN.safeTransferFrom(msg.sender, address(this), amount);

uint256 amountAdjusted = amount / (10 ** (ERC20_TOKEN.decimals() - decimals()));

if (amountAdjusted > type(uint64).max) {
revert AmountTooHigh();
}

uint64 amountUint64 = uint64(amountAdjusted);

_unsafeMint(msg.sender, amountUint64);
_totalSupply += amountUint64;

emit Wrap(msg.sender, amountUint64);
}

/**
* @notice Callback function for the gateway.
* @param requestId Request id.
* @param canUnwrap Whether it can be unwrapped.
*/
function callbackUnwrap(uint256 requestId, bool canUnwrap) public virtual nonReentrant onlyGateway {
UnwrapRequest memory unwrapRequest = unwrapRequests[requestId];

if (canUnwrap) {
/// @dev It does a supply adjustment.
uint256 amountUint256 = unwrapRequest.amount * (10 ** (ERC20_TOKEN.decimals() - decimals()));

try ERC20_TOKEN.transfer(unwrapRequest.account, amountUint256) {
_unsafeBurn(unwrapRequest.account, unwrapRequest.amount);
_totalSupply -= unwrapRequest.amount;
emit Unwrap(unwrapRequest.account, unwrapRequest.amount);
} catch {
emit UnwrapFailTransferFail(unwrapRequest.account, unwrapRequest.amount);
}
} else {
emit UnwrapFailNotEnoughBalance(unwrapRequest.account, unwrapRequest.amount);
}

delete unwrapRequests[requestId];
delete isAccountRestricted[unwrapRequest.account];
}

function _canTransferOrUnwrap(address account) internal virtual {
if (isAccountRestricted[account]) {
revert CannotTransferOrUnwrap();
}
}

function _transferNoEvent(
address from,
address to,
euint64 amount,
ebool isTransferable
) internal virtual override {
_canTransferOrUnwrap(from);
super._transferNoEvent(from, to, amount, isTransferable);
}

function _unsafeBurn(address account, uint64 amount) internal {
euint64 newBalanceAccount = TFHE.sub(_balances[account], amount);
_balances[account] = newBalanceAccount;
TFHE.allowThis(newBalanceAccount);
TFHE.allow(newBalanceAccount, account);
emit Transfer(account, address(0), _PLACEHOLDER);
}
}
Loading

0 comments on commit 4917e07

Please sign in to comment.