From e67da48c25e24112d9dd6d02913a3df762111981 Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 14 Nov 2024 12:32:29 -0500 Subject: [PATCH] Add in transfers app (#282) Co-authored-by: Dave Kaj --- .../evm/contracts/.github/workflows/test.yml | 45 --- crates/evm/contracts/src/Quartz.sol | 40 +-- crates/evm/contracts/src/Transfers.sol | 175 ++++++++++ .../contracts/src/openzeppelin/Context.sol | 28 ++ .../evm/contracts/src/openzeppelin/ERC20.sol | 312 ++++++++++++++++++ .../evm/contracts/src/openzeppelin/IERC20.sol | 79 +++++ .../src/openzeppelin/IERC20Metadata.sol | 26 ++ .../contracts/src/openzeppelin/Ownable.sol | 100 ++++++ .../src/openzeppelin/draft-IERC6093.sol | 162 +++++++++ crates/evm/contracts/test/MockERC20.sol | 26 ++ crates/evm/contracts/test/Quartz.t.sol | 2 - crates/evm/contracts/test/Transfers.t.sol | 185 +++++++++++ 12 files changed, 1114 insertions(+), 66 deletions(-) delete mode 100644 crates/evm/contracts/.github/workflows/test.yml create mode 100644 crates/evm/contracts/src/Transfers.sol create mode 100644 crates/evm/contracts/src/openzeppelin/Context.sol create mode 100644 crates/evm/contracts/src/openzeppelin/ERC20.sol create mode 100644 crates/evm/contracts/src/openzeppelin/IERC20.sol create mode 100644 crates/evm/contracts/src/openzeppelin/IERC20Metadata.sol create mode 100644 crates/evm/contracts/src/openzeppelin/Ownable.sol create mode 100644 crates/evm/contracts/src/openzeppelin/draft-IERC6093.sol create mode 100644 crates/evm/contracts/test/MockERC20.sol create mode 100644 crates/evm/contracts/test/Transfers.t.sol diff --git a/crates/evm/contracts/.github/workflows/test.yml b/crates/evm/contracts/.github/workflows/test.yml deleted file mode 100644 index 762a2966..00000000 --- a/crates/evm/contracts/.github/workflows/test.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: CI - -on: - push: - pull_request: - workflow_dispatch: - -env: - FOUNDRY_PROFILE: ci - -jobs: - check: - strategy: - fail-fast: true - - name: Foundry project - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly - - - name: Show Forge version - run: | - forge --version - - - name: Run Forge fmt - run: | - forge fmt --check - id: fmt - - - name: Run Forge build - run: | - forge build --sizes - id: build - - - name: Run Forge tests - run: | - forge test -vvv - id: test diff --git a/crates/evm/contracts/src/Quartz.sol b/crates/evm/contracts/src/Quartz.sol index 5499bc8c..b8964037 100644 --- a/crates/evm/contracts/src/Quartz.sol +++ b/crates/evm/contracts/src/Quartz.sol @@ -33,6 +33,21 @@ contract Quartz { event SessionCreated(address indexed quartz); event PubKeySet(bytes32 indexed enclavePubKey); + /** + * @dev Modifier that verifies the caller's authenticity through an enclave-attested quote. + * Reverts with a specific error message if attestation fails. + * @param _quote The attestation quote used to verify the caller's enclave status. + */ + modifier onlyEnclave(bytes memory _quote) { + (bool success, bytes memory output) = attest.verifyAndAttestOnChain(_quote); + if (success) { + _; + } else { + string memory errorMessage = _getRevertMessage(output); + revert(errorMessage); + } + } + /** * @notice Initializes the Quartz contract with the config, attests it's from a DCAP enclave, * and emits an event for the host to listen to @@ -42,18 +57,11 @@ contract Quartz { * @param _config The configuration object for the light client * @param _quote The DCAP attestation quote provided by the enclave * Emits a {SessionCreated} event upon successful verification. - * Reverts with an error message if `verifyAndAttestOnChain` fails. + * Reverts as per onlyEnclave() */ - constructor(Config memory _config, bytes memory _quote) { + constructor(Config memory _config, bytes memory _quote) onlyEnclave(_quote) { config = _config; - (bool success, bytes memory output) = attest.verifyAndAttestOnChain(_quote); - - if (success) { - emit SessionCreated(address(this)); - } else { - string memory errorMessage = _getRevertMessage(output); - revert(errorMessage); - } + emit SessionCreated(address(this)); } /** @@ -65,15 +73,9 @@ contract Quartz { * Emits a {PubKeySet} event upon successful setting of the public key * Reverts with an error message if `verifyAndAttestOnChain` fails to verify the attestation */ - function setSessionPubKey(bytes32 _pubKey, bytes memory _quote) external { - (bool success, bytes memory output) = attest.verifyAndAttestOnChain(_quote); - if (success) { - enclavePubKey = _pubKey; - emit PubKeySet(enclavePubKey); - } else { - string memory errorMessage = _getRevertMessage(output); - revert(errorMessage); - } + function setSessionPubKey(bytes32 _pubKey, bytes memory _quote) external onlyEnclave(_quote) { + enclavePubKey = _pubKey; + emit PubKeySet(enclavePubKey); } // TODO - Implement sequence number incrementing... but I assume we should have the transfers or ping pong app do this diff --git a/crates/evm/contracts/src/Transfers.sol b/crates/evm/contracts/src/Transfers.sol new file mode 100644 index 00000000..cd40d034 --- /dev/null +++ b/crates/evm/contracts/src/Transfers.sol @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.13; + +import "./Quartz.sol"; +import "./openzeppelin/IERC20.sol"; + +/** + * @title Transfers + * @notice A token transfer application utilizing a Trusted Execution Environment (TEE) enclave for + * encrypted state management. + * + * @dev This contract enables users to transfer ERC20 tokens with the following features: + * - Unencrypted deposits and withdrawals: ERC20 transfers and `msg.sender` visibility prevent full + * encryption of these actions on-chain. + * - Encrypted transfers: all transfers are encrypted within the enclave + * - Encrypted balance: Token balances are stored encrypted in the contract + * - Event-based update mechanism: + * - Each transfer, deposit, or withdrawal triggers an event that the enclave monitors. + * - Upon detecting an event, the enclave responds by calling update() to clear pending requests and + * process withdrawals. + * - Multiple requests per block: When there are multiple transfer, deposit, or withdrawal requests in a + * block, they are handled collectively. + * - Querying Capabilities: Provides rudimentary querying, where users query the enclave and it will + * store the encryptedBalance with the ephemeralPubkey the user provided. + */ +contract Transfers is Quartz { + IERC20 public token; + address public owner; + + /// @dev Struct to represent a request, with a type indicator and associated data + /// Only certain params are used for each request type, to allow for the struct + /// to represent all types of requests, as rust can (Vec can hold all types) + struct Request { + Action action; + address user; // Used for Withdraw and Deposit + uint256 amount; // Used for Deposit + bytes32 ciphertext; // Used for Transfer type (encrypted data) + } + + enum Action { + DEPOSIT, + WITHDRAW, + TRANSFER + } + + // User initiated events + event Deposit(address indexed user, uint256 amount); + event WithdrawRequest(address indexed user); + event TransferRequest(address indexed sender, bytes32 ciphertext); + event QueryRequestMessage(address indexed user, bytes ephemeralPubkey); + event UpdateRequestMessage(uint256 indexed sequenceNum, bytes newEncryptedState, Transfers.Request[] requests); + + // Enclave initiated events + event WithdrawResponse(address indexed user, uint256 amount); + event EncryptedBalanceStored(address indexed user, bytes encryptedBalance); + event StateUpdated(bytes newEncryptedState); + + // TODO - nat spec this + mapping(address => bytes) public encryptedBalances; + Request[] private requests; + bytes public encryptedState; + + /** + * @notice Initializes the Transfers contract with the Quartz configuration and token address. + * @param _config The configuration object for Quartz + * @param _quote The attestation quote for Quartz setup + * @param _token The ERC20 token used + */ + constructor(Config memory _config, bytes memory _quote, address _token) Quartz(_config, _quote) { + token = IERC20(_token); + owner = msg.sender; + } + + /** + * @notice Deposits tokens to the contract. Enclave will watch for UpdateRequestMessage(), and + * then call update() to process the deposit. + */ + function deposit(uint256 amount) external { + require(token.transferFrom(msg.sender, address(this), amount), "Transfer failed"); + requests.push(Request(Action.DEPOSIT, msg.sender, amount, bytes32(0))); + emit Deposit(msg.sender, amount); + emit UpdateRequestMessage(sequenceNum, encryptedState, requests); + sequenceNum++; + } + + /** + * @notice Requests to withdraw *all* tokens from the caller's balance. Enclave will watch for + * UpdateRequestMessage(), and then call update() to process the withdrawal. + */ + function withdraw() external { + requests.push(Request(Action.WITHDRAW, msg.sender, 0, bytes32(0))); + emit WithdrawRequest(msg.sender); + emit UpdateRequestMessage(sequenceNum, encryptedState, requests); + sequenceNum++; + } + + /** + * @notice Requests a transfer with encrypted ciphertext. Enclave will watch for + * UpdateRequestMessage(), and then call update() to process the transfer. + * @param ciphertext The encrypted transfer data (encrypted by the enclave pub key) + */ + function transferRequest(bytes32 ciphertext) external { + requests.push(Request(Action.TRANSFER, msg.sender, 0, ciphertext)); + emit TransferRequest(msg.sender, ciphertext); + emit UpdateRequestMessage(sequenceNum, encryptedState, requests); + sequenceNum++; + } + + /** + * @notice Updates the contract state with a new encrypted state, clears requests, and processes + * withdrawals. + * @dev Only enclave can call this function + * @param newEncryptedState The new encrypted state to be stored. + * @param withdrawalAddresses The list of addresses requesting withdrawals. + * @param withdrawalAmounts The corresponding list of withdrawal amounts for each address. + * @param quote The attestation quote for enclave verification. + */ + function update( + bytes memory newEncryptedState, + address[] calldata withdrawalAddresses, + uint256[] calldata withdrawalAmounts, + bytes memory quote + ) external onlyEnclave(quote) { + require(withdrawalAddresses.length == withdrawalAmounts.length, "Mismatched withdrawals"); + + // Store the new encrypted state + encryptedState = newEncryptedState; + emit StateUpdated(newEncryptedState); + + // Clear stored requests + delete requests; + + // Process each withdrawal + for (uint256 i = 0; i < withdrawalAddresses.length; i++) { + address user = withdrawalAddresses[i]; + uint256 amount = withdrawalAmounts[i]; + require(token.transfer(user, amount), "Transfer failed"); + emit WithdrawResponse(user, amount); + } + } + /** + * @notice User calls this have their encrypted balance stored in the contract. + * Enclave will watch for QueryRequestMessage(), and then call storeEncryptedBalance() to + * store the balance. + * @param ephemeralPubley The pubkey used to decrypt the stored balance + */ + + function queryEncryptedBalance(bytes memory ephemeralPubley) public { + emit QueryRequestMessage(msg.sender, ephemeralPubley); + } + + /** + * @notice Stores an encrypted balance for a user, restricted to enclave calls. + * @param user The address of the user whose balance is being stored + * @param encryptedBalance The encrypted balance data + * @param quote The attestation quote for enclave verification + */ + function storeEncryptedBalance(address user, bytes memory encryptedBalance, bytes memory quote) + external + onlyEnclave(quote) + { + encryptedBalances[user] = encryptedBalance; + emit EncryptedBalanceStored(user, encryptedBalance); + } + + function getRequest(uint256 index) external view returns (Transfers.Request memory) { + return requests[index]; + } + + /// @notice Returns the entire list of requests + /// @return All requests stored in the contract + function getAllRequests() public view returns (Request[] memory) { + return requests; + } +} diff --git a/crates/evm/contracts/src/openzeppelin/Context.sol b/crates/evm/contracts/src/openzeppelin/Context.sol new file mode 100644 index 00000000..3981b60e --- /dev/null +++ b/crates/evm/contracts/src/openzeppelin/Context.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } + + function _contextSuffixLength() internal view virtual returns (uint256) { + return 0; + } +} \ No newline at end of file diff --git a/crates/evm/contracts/src/openzeppelin/ERC20.sol b/crates/evm/contracts/src/openzeppelin/ERC20.sol new file mode 100644 index 00000000..295c719e --- /dev/null +++ b/crates/evm/contracts/src/openzeppelin/ERC20.sol @@ -0,0 +1,312 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/ERC20.sol) + +pragma solidity ^0.8.20; + +import {IERC20} from "./IERC20.sol"; +import {IERC20Metadata} from "./IERC20Metadata.sol"; +import {Context} from "./Context.sol"; +import {IERC20Errors} from "./draft-IERC6093.sol"; + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * + * TIP: For a detailed writeup see our guide + * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * The default value of {decimals} is 18. To change this, you should override + * this function so it returns a different value. + * + * We have followed general OpenZeppelin Contracts guidelines: functions revert + * instead returning `false` on failure. This behavior is nonetheless + * conventional and does not conflict with the expectations of ERC-20 + * applications. + */ +abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors { + mapping(address account => uint256) private _balances; + + mapping(address account => mapping(address spender => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + + /** + * @dev Sets the values for {name} and {symbol}. + * + * All two of these values are immutable: they can only be set once during + * construction. + */ + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view virtual returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the default value returned by this function, unless + * it's overridden. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view virtual returns (uint8) { + return 18; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view virtual returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view virtual returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `value`. + */ + function transfer(address to, uint256 value) public virtual returns (bool) { + address owner = _msgSender(); + _transfer(owner, to, value); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view virtual returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: If `value` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 value) public virtual returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, value); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Skips emitting an {Approval} event indicating an allowance update. This is not + * required by the ERC. See {xref-ERC20-_approve-address-address-uint256-bool-}[_approve]. + * + * NOTE: Does not update the allowance if the current allowance + * is the maximum `uint256`. + * + * Requirements: + * + * - `from` and `to` cannot be the zero address. + * - `from` must have a balance of at least `value`. + * - the caller must have allowance for ``from``'s tokens of at least + * `value`. + */ + function transferFrom(address from, address to, uint256 value) public virtual returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, value); + _transfer(from, to, value); + return true; + } + + /** + * @dev Moves a `value` amount of tokens from `from` to `to`. + * + * This internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * NOTE: This function is not virtual, {_update} should be overridden instead. + */ + function _transfer(address from, address to, uint256 value) internal { + if (from == address(0)) { + revert ERC20InvalidSender(address(0)); + } + if (to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + _update(from, to, value); + } + + /** + * @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from` + * (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding + * this function. + * + * Emits a {Transfer} event. + */ + function _update(address from, address to, uint256 value) internal virtual { + if (from == address(0)) { + // Overflow check required: The rest of the code assumes that totalSupply never overflows + _totalSupply += value; + } else { + uint256 fromBalance = _balances[from]; + if (fromBalance < value) { + revert ERC20InsufficientBalance(from, fromBalance, value); + } + unchecked { + // Overflow not possible: value <= fromBalance <= totalSupply. + _balances[from] = fromBalance - value; + } + } + + if (to == address(0)) { + unchecked { + // Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply. + _totalSupply -= value; + } + } else { + unchecked { + // Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256. + _balances[to] += value; + } + } + + emit Transfer(from, to, value); + } + + /** + * @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0). + * Relies on the `_update` mechanism + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * NOTE: This function is not virtual, {_update} should be overridden instead. + */ + function _mint(address account, uint256 value) internal { + if (account == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + _update(address(0), account, value); + } + + /** + * @dev Destroys a `value` amount of tokens from `account`, lowering the total supply. + * Relies on the `_update` mechanism. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * NOTE: This function is not virtual, {_update} should be overridden instead + */ + function _burn(address account, uint256 value) internal { + if (account == address(0)) { + revert ERC20InvalidSender(address(0)); + } + _update(account, address(0), value); + } + + /** + * @dev Sets `value` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + * + * Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument. + */ + function _approve(address owner, address spender, uint256 value) internal { + _approve(owner, spender, value, true); + } + + /** + * @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event. + * + * By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by + * `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any + * `Approval` event during `transferFrom` operations. + * + * Anyone who wishes to continue emitting `Approval` events on the`transferFrom` operation can force the flag to + * true using the following override: + * + * ```solidity + * function _approve(address owner, address spender, uint256 value, bool) internal virtual override { + * super._approve(owner, spender, value, true); + * } + * ``` + * + * Requirements are the same as {_approve}. + */ + function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual { + if (owner == address(0)) { + revert ERC20InvalidApprover(address(0)); + } + if (spender == address(0)) { + revert ERC20InvalidSpender(address(0)); + } + _allowances[owner][spender] = value; + if (emitEvent) { + emit Approval(owner, spender, value); + } + } + + /** + * @dev Updates `owner` s allowance for `spender` based on spent `value`. + * + * Does not update the allowance value in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Does not emit an {Approval} event. + */ + function _spendAllowance(address owner, address spender, uint256 value) internal virtual { + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance < type(uint256).max) { + if (currentAllowance < value) { + revert ERC20InsufficientAllowance(spender, currentAllowance, value); + } + unchecked { + _approve(owner, spender, currentAllowance - value, false); + } + } + } +} \ No newline at end of file diff --git a/crates/evm/contracts/src/openzeppelin/IERC20.sol b/crates/evm/contracts/src/openzeppelin/IERC20.sol new file mode 100644 index 00000000..7fea97e8 --- /dev/null +++ b/crates/evm/contracts/src/openzeppelin/IERC20.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Interface of the ERC-20 standard as defined in the ERC. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the value of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the value of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 value) 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 allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets a `value` amount of 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 {Approval} event. + */ + function approve(address spender, uint256 value) external returns (bool); + + /** + * @dev Moves a `value` amount of tokens from `from` to `to` using the + * allowance mechanism. `value` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 value) external returns (bool); +} \ No newline at end of file diff --git a/crates/evm/contracts/src/openzeppelin/IERC20Metadata.sol b/crates/evm/contracts/src/openzeppelin/IERC20Metadata.sol new file mode 100644 index 00000000..6730a8fd --- /dev/null +++ b/crates/evm/contracts/src/openzeppelin/IERC20Metadata.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/IERC20Metadata.sol) + +pragma solidity ^0.8.20; + +import {IERC20} from "./IERC20.sol"; + +/** + * @dev Interface for the optional metadata functions from the ERC-20 standard. + */ +interface IERC20Metadata is IERC20 { + /** + * @dev Returns the name of the token. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the symbol of the token. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the decimals places of the token. + */ + function decimals() external view returns (uint8); +} \ No newline at end of file diff --git a/crates/evm/contracts/src/openzeppelin/Ownable.sol b/crates/evm/contracts/src/openzeppelin/Ownable.sol new file mode 100644 index 00000000..45aaf9f2 --- /dev/null +++ b/crates/evm/contracts/src/openzeppelin/Ownable.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol) + +pragma solidity ^0.8.20; + +import {Context} from "./Context.sol"; + +/** + * @dev Contract module which provides a basic access control mechanism, where + * there is an account (an owner) that can be granted exclusive access to + * specific functions. + * + * The initial owner is set to the address provided by the deployer. This can + * later be changed with {transferOwnership}. + * + * This module is used through inheritance. It will make available the modifier + * `onlyOwner`, which can be applied to your functions to restrict their use to + * the owner. + */ +abstract contract Ownable is Context { + address private _owner; + + /** + * @dev The caller account is not authorized to perform an operation. + */ + error OwnableUnauthorizedAccount(address account); + + /** + * @dev The owner is not a valid owner account. (eg. `address(0)`) + */ + error OwnableInvalidOwner(address owner); + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /** + * @dev Initializes the contract setting the address provided by the deployer as the initial owner. + */ + constructor(address initialOwner) { + if (initialOwner == address(0)) { + revert OwnableInvalidOwner(address(0)); + } + _transferOwnership(initialOwner); + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + _checkOwner(); + _; + } + + /** + * @dev Returns the address of the current owner. + */ + function owner() public view virtual returns (address) { + return _owner; + } + + /** + * @dev Throws if the sender is not the owner. + */ + function _checkOwner() internal view virtual { + if (owner() != _msgSender()) { + revert OwnableUnauthorizedAccount(_msgSender()); + } + } + + /** + * @dev Leaves the contract without owner. It will not be possible to call + * `onlyOwner` functions. Can only be called by the current owner. + * + * NOTE: Renouncing ownership will leave the contract without an owner, + * thereby disabling any functionality that is only available to the owner. + */ + function renounceOwnership() public virtual onlyOwner { + _transferOwnership(address(0)); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current owner. + */ + function transferOwnership(address newOwner) public virtual onlyOwner { + if (newOwner == address(0)) { + revert OwnableInvalidOwner(address(0)); + } + _transferOwnership(newOwner); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Internal function without access restriction. + */ + function _transferOwnership(address newOwner) internal virtual { + address oldOwner = _owner; + _owner = newOwner; + emit OwnershipTransferred(oldOwner, newOwner); + } +} \ No newline at end of file diff --git a/crates/evm/contracts/src/openzeppelin/draft-IERC6093.sol b/crates/evm/contracts/src/openzeppelin/draft-IERC6093.sol new file mode 100644 index 00000000..dba877cd --- /dev/null +++ b/crates/evm/contracts/src/openzeppelin/draft-IERC6093.sol @@ -0,0 +1,162 @@ + +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/draft-IERC6093.sol) +pragma solidity ^0.8.20; + +/** + * @dev Standard ERC-20 Errors + * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-20 tokens. + */ +interface IERC20Errors { + /** + * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param balance Current balance for the interacting account. + * @param needed Minimum amount required to perform a transfer. + */ + error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC20InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC20InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `spender`’s `allowance`. Used in transfers. + * @param spender Address that may be allowed to operate on tokens without being their owner. + * @param allowance Amount of tokens a `spender` is allowed to operate with. + * @param needed Minimum amount required to perform a transfer. + */ + error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC20InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `spender` to be approved. Used in approvals. + * @param spender Address that may be allowed to operate on tokens without being their owner. + */ + error ERC20InvalidSpender(address spender); +} + +/** + * @dev Standard ERC-721 Errors + * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-721 tokens. + */ +interface IERC721Errors { + /** + * @dev Indicates that an address can't be an owner. For example, `address(0)` is a forbidden owner in ERC-20. + * Used in balance queries. + * @param owner Address of the current owner of a token. + */ + error ERC721InvalidOwner(address owner); + + /** + * @dev Indicates a `tokenId` whose `owner` is the zero address. + * @param tokenId Identifier number of a token. + */ + error ERC721NonexistentToken(uint256 tokenId); + + /** + * @dev Indicates an error related to the ownership over a particular token. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param tokenId Identifier number of a token. + * @param owner Address of the current owner of a token. + */ + error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC721InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC721InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `operator`’s approval. Used in transfers. + * @param operator Address that may be allowed to operate on tokens without being their owner. + * @param tokenId Identifier number of a token. + */ + error ERC721InsufficientApproval(address operator, uint256 tokenId); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC721InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `operator` to be approved. Used in approvals. + * @param operator Address that may be allowed to operate on tokens without being their owner. + */ + error ERC721InvalidOperator(address operator); +} + +/** + * @dev Standard ERC-1155 Errors + * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-1155 tokens. + */ +interface IERC1155Errors { + /** + * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param balance Current balance for the interacting account. + * @param needed Minimum amount required to perform a transfer. + * @param tokenId Identifier number of a token. + */ + error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC1155InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC1155InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `operator`’s approval. Used in transfers. + * @param operator Address that may be allowed to operate on tokens without being their owner. + * @param owner Address of the current owner of a token. + */ + error ERC1155MissingApprovalForAll(address operator, address owner); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC1155InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `operator` to be approved. Used in approvals. + * @param operator Address that may be allowed to operate on tokens without being their owner. + */ + error ERC1155InvalidOperator(address operator); + + /** + * @dev Indicates an array length mismatch between ids and values in a safeBatchTransferFrom operation. + * Used in batch transfers. + * @param idsLength Length of the array of token identifiers + * @param valuesLength Length of the array of token amounts + */ + error ERC1155InvalidArrayLength(uint256 idsLength, uint256 valuesLength); +} diff --git a/crates/evm/contracts/test/MockERC20.sol b/crates/evm/contracts/test/MockERC20.sol new file mode 100644 index 00000000..12663a9c --- /dev/null +++ b/crates/evm/contracts/test/MockERC20.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "../src/openzeppelin/ERC20.sol"; + +/** + * @title MockERC20 + * @dev ERC20 token with a public mint function that can be called by anyone. + */ +contract MockERC20 is ERC20 { + /** + * @dev Constructor that initializes the token with a name and symbol. + * @param name_ The name of the token. + * @param symbol_ The symbol of the token. + */ + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} + + /** + * @dev Mints `amount` tokens to the specified `account`. + * @param account The address to which the tokens will be minted. + * @param amount The amount of tokens to mint. + */ + function mint(address account, uint256 amount) external { + _mint(account, amount); + } +} diff --git a/crates/evm/contracts/test/Quartz.t.sol b/crates/evm/contracts/test/Quartz.t.sol index ab4ab27e..c1db50fb 100644 --- a/crates/evm/contracts/test/Quartz.t.sol +++ b/crates/evm/contracts/test/Quartz.t.sol @@ -22,8 +22,6 @@ contract QuartzTest is Test { event PubKeySet(bytes32 indexed enclavePubKey); function setUp() public { - console.log("Test Suite started!!"); - // Set up the dummy LightClientOpts Quartz.LightClientOpts memory lightClientOpts = Quartz.LightClientOpts({ chainID: dummyChainID, diff --git a/crates/evm/contracts/test/Transfers.t.sol b/crates/evm/contracts/test/Transfers.t.sol new file mode 100644 index 00000000..e84816a3 --- /dev/null +++ b/crates/evm/contracts/test/Transfers.t.sol @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "../src/Transfers.sol"; +import "./MockERC20.sol"; + +contract TransfersTest is Test { + bytes32 dummyMrEnclave = bytes32("dummyMrEnclave"); + string dummyChainID = "dummyChainID"; + uint256 dummyTrustedHeight = 100; + bytes32 dummyTrustedHash = bytes32("dummyTrustedHash"); + address dummyPccs = address(0x1234567890123456789012345678901234567890); + bytes dummyQuote = + hex"03000200000000000b001000939a7233f79c4ca9940a0db3957f06077944f37bdafec57cf7d4ab6bc395e0a1000000000e0e100fffff0100000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000e70000000000000081f36e827391dc0916f06215400b32a5b2823c4c3349428a50dbc531cfdad5a40000000000000000000000000000000000000000000000000000000000000000255197a6388e504446dbf83726c2a9cb3cef9035cc3dabd6cf47d69a994f95940000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ca100000dd653050dd320cc49517a0b858fad3587e4050121e074c671e5d396a0acb402dc0d01648efa8ac87f47a1abb66a45c664dfad325c40628037303d6821f8c67d453b560130d79fd96a0508e1df00f67064f302ea0e1c8226764a5fc7fe5bc8c88aa8fe5d18a064a2ceb780cefa8b5daff5346516784b11dac7464bc641cbf16810e0e100fffff0100000000000000000000000000000000000000000000000000000000000000000000000000000000001500000000000000e70000000000000078fe8cfd01095a0f108aff5c40624b93612d6c28b73e1a8d28179c9ddf0e068600000000000000000000000000000000000000000000000000000000000000008c4f5775d796503e96137f77c68a829a0056ac8ded70140b081b094490c57bff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008c8b256a3f090a680d3afb94dbd79aa100f305368b3ed48fcacfe80bec2bef580000000000000000000000000000000000000000000000000000000000000000964c950f663a505a471573a7b5ad7f80f76292568231cfc6552dcef204459cae9dda1b5bfa1379620fb893e6326a35473d55724373127d6620d693e54cc05dc22000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f0500620e00002d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494945386a4343424a696741774942416749554854525a7054426e4d59655063575531354542316643507561485177436759494b6f5a497a6a3045417749770a634445694d434147413155454177775a535735305a577767553064594946424453794251624746305a6d397962534244515445614d42674741315545436777520a535735305a577767513239796347397959585270623234784644415342674e564241634d43314e68626e526849454e7359584a684d51737743515944565151490a44414a445154454c4d416b474131554542684d4356564d774868634e4d6a51774e5445344d5449314e4451325768634e4d7a45774e5445344d5449314e4451320a576a42774d534977494159445651514444426c4a626e526c624342545231676755454e4c49454e6c636e52705a6d6c6a5958526c4d526f77474159445651514b0a4442464a626e526c6243424462334a7762334a6864476c76626a45554d424947413155454277774c553246756447456751327868636d4578437a414a42674e560a4241674d416b4e424d517377435159445651514745774a56557a425a4d424d4742797147534d34394167454743437147534d34394177454841304941424f64700a696252626a7639566159476a7159584a766f7670333253752f594861313541727943546735566c384762744348417a396e5952786d4e6a303372553548687a4d0a513030752b364a6d794748744b4f773866364f6a67674d4f4d494944436a416642674e5648534d4547444157674253566231334e765276683655424a796454300a4d383442567776655644427242674e56485238455a4442694d47436758714263686c706f64485277637a6f764c32467761533530636e567a6447566b633256790a646d6c6a5a584d75615735305a577775593239744c334e6e6543396a5a584a3061575a7059324630615739754c3359304c33426a61324e796244396a595431770a624746305a6d397962535a6c626d4e765a476c755a7a316b5a584977485159445652304f42425945464167706b386e6b7a4c6371776b6749376f7567574844560a574d67314d41344741315564447745422f775145417749477744414d42674e5648524d4241663845416a41414d4949434f77594a4b6f5a496876684e415130420a424949434c444343416967774867594b4b6f5a496876684e415130424151515151576f736c643645563066526f62747368385149737a434341575547436971470a534962345451454e41514977676746564d42414743797147534962345451454e415149424167454f4d42414743797147534962345451454e415149434167454f0a4d42414743797147534962345451454e41514944416745444d42414743797147534962345451454e41514945416745444d42454743797147534962345451454e0a41514946416749412f7a415242677371686b69472b4530424451454342674943415038774541594c4b6f5a496876684e4151304241676343415145774541594c0a4b6f5a496876684e4151304241676743415141774541594c4b6f5a496876684e4151304241676b43415141774541594c4b6f5a496876684e4151304241676f430a415141774541594c4b6f5a496876684e4151304241677343415141774541594c4b6f5a496876684e4151304241677743415141774541594c4b6f5a496876684e0a4151304241673043415141774541594c4b6f5a496876684e4151304241673443415141774541594c4b6f5a496876684e4151304241673843415141774541594c0a4b6f5a496876684e4151304241684143415141774541594c4b6f5a496876684e4151304241684543415130774877594c4b6f5a496876684e41513042416849450a4541344f4177502f2f7745414141414141414141414141774541594b4b6f5a496876684e4151304241775143414141774641594b4b6f5a496876684e415130420a4241514741474271414141414d41384743697147534962345451454e4151554b415145774867594b4b6f5a496876684e4151304242675151446758512b3446660a2b6c2b4853522f457161474d737a424542676f71686b69472b453042445145484d4459774541594c4b6f5a496876684e4151304242774542416638774541594c0a4b6f5a496876684e4151304242774942415141774541594c4b6f5a496876684e4151304242774d4241514177436759494b6f5a497a6a304541774944534141770a52514968414c3047436752526b30764e6c585a594e506d5738634f313632364c4353332f2f4c6d6f416638756a4457484169426d41324d56347058774f386d6d0a4171444e4c345a6843792f64657a4842796c746f307271377149664c51773d3d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d4949436c6a4343416a32674177494241674956414a567658633239472b487051456e4a3150517a7a674658433935554d416f4743437147534d343942414d430a4d476778476a415942674e5642414d4d45556c756447567349464e48574342536232393049454e424d526f77474159445651514b4442464a626e526c624342440a62334a7762334a6864476c76626a45554d424947413155454277774c553246756447456751327868636d4578437a414a42674e564241674d416b4e424d5173770a435159445651514745774a56557a4165467730784f4441314d6a45784d4455774d5442614677307a4d7a41314d6a45784d4455774d5442614d484178496a41670a42674e5642414d4d47556c756447567349464e4857434251513073675547786864475a76636d306751304578476a415942674e5642416f4d45556c75644756730a49454e76636e4276636d4630615739754d5251774567594456515148444174545957353059534244624746795954454c4d416b474131554543417743513045780a437a414a42674e5642415954416c56544d466b77457759484b6f5a497a6a3043415159494b6f5a497a6a304441516344516741454e53422f377432316c58534f0a3243757a7078773734654a423732457944476757357258437478327456544c7136684b6b367a2b5569525a436e71523770734f766771466553786c6d546c4a6c0a65546d693257597a33714f42757a43427544416642674e5648534d4547444157674251695a517a575770303069664f44744a5653763141624f536347724442530a42674e5648523845537a424a4d45656752614244686b466f64485277637a6f764c324e6c636e52705a6d6c6a5958526c63793530636e567a6447566b633256790a646d6c6a5a584d75615735305a577775593239744c306c756447567355306459556d397664454e424c6d526c636a416442674e5648513445466751556c5739640a7a62306234656c4153636e553944504f4156634c336c517744675944565230504151482f42415144416745474d42494741315564457745422f7751494d4159420a4166384341514177436759494b6f5a497a6a30454177494452774177524149675873566b6930772b6936565947573355462f32327561586530594a446a3155650a6e412b546a44316169356343494359623153416d4435786b66545670766f34556f79695359787244574c6d5552344349394e4b7966504e2b0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d4949436a7a4343416a53674177494241674955496d554d316c71644e496e7a6737535655723951477a6b6e42717777436759494b6f5a497a6a3045417749770a614445614d4267474131554541777752535735305a5777675530645949464a766233516751304578476a415942674e5642416f4d45556c756447567349454e760a636e4276636d4630615739754d5251774567594456515148444174545957353059534244624746795954454c4d416b47413155454341774351304578437a414a0a42674e5642415954416c56544d423458445445344d4455794d5445774e4455784d466f58445451354d54497a4d54497a4e546b314f566f77614445614d4267470a4131554541777752535735305a5777675530645949464a766233516751304578476a415942674e5642416f4d45556c756447567349454e76636e4276636d46300a615739754d5251774567594456515148444174545957353059534244624746795954454c4d416b47413155454341774351304578437a414a42674e56424159540a416c56544d466b77457759484b6f5a497a6a3043415159494b6f5a497a6a3044415163445167414543366e45774d4449595a4f6a2f69505773437a61454b69370a314f694f534c52466857476a626e42564a66566e6b59347533496a6b4459594c304d784f346d717379596a6c42616c54565978465032734a424b357a6c4b4f420a757a43427544416642674e5648534d4547444157674251695a517a575770303069664f44744a5653763141624f5363477244425342674e5648523845537a424a0a4d45656752614244686b466f64485277637a6f764c324e6c636e52705a6d6c6a5958526c63793530636e567a6447566b63325679646d6c6a5a584d75615735300a5a577775593239744c306c756447567355306459556d397664454e424c6d526c636a416442674e564851344546675155496d554d316c71644e496e7a673753560a55723951477a6b6e4271777744675944565230504151482f42415144416745474d42494741315564457745422f7751494d4159424166384341514577436759490a4b6f5a497a6a3045417749445351417752674968414f572f35516b522b533943695344634e6f6f774c7550524c735747662f59693747535839344267775477670a41694541344a306c72486f4d732b586f356f2f7358364f39515778485241765a55474f6452513763767152586171493d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a00"; // Replace with appropriate format if needed + + Quartz.Config dummyConfig; + Transfers public transfers; + MockERC20 public token; + address public user1 = 0x1111111111111111111111111111111111111111; + address public user2 = 0x2222222222222222222222222222222222222222; + + // User initiated events + event Deposit(address indexed user, uint256 amount); + event WithdrawRequest(address indexed user); + event TransferRequest(address indexed sender, bytes32 ciphertext); + event QueryRequestMessage(address indexed user, bytes ephemeralPubkey); + event UpdateRequestMessage(uint256 indexed sequenceNum, bytes newEncryptedState, Transfers.Request[] requests); + + // Enclave initiated events + event WithdrawResponse(address indexed user, uint256 amount); + event EncryptedBalanceStored(address indexed user, bytes encryptedBalance); + event StateUpdated(bytes newEncryptedState); + + + function setUp() public { + // Set up the dummy LightClientOpts + Quartz.LightClientOpts memory lightClientOpts = Quartz.LightClientOpts({ + chainID: dummyChainID, + trustedHeight: dummyTrustedHeight, + trustedHash: dummyTrustedHash + }); + + // Set up the dummy Config + dummyConfig = Quartz.Config({mrEnclave: dummyMrEnclave, lightClientOpts: lightClientOpts, pccs: dummyPccs}); + + // Deploy mock token and mint some tokens to users + token = new MockERC20("Cycles", "CYC"); + token.mint(address(this), 1000 ether); + token.mint(user1, 500 ether); + token.mint(user2, 500 ether); + + // Deploy the Transfers contract + transfers = new Transfers(dummyConfig, dummyQuote, address(token)); + } + + function testDeposit() public { + uint256 depositAmount = 50 ether; + vm.prank(user1); + token.approve(address(transfers), depositAmount); + uint256 seqBefore = transfers.sequenceNum(); + uint256 balanceBefore = token.balanceOf(address(transfers)); + + Transfers.Request[] memory requests = new Transfers.Request[](1); + requests[0] = Transfers.Request(Transfers.Action.DEPOSIT, user1, depositAmount, bytes32(0)); + + vm.expectEmit(true, false, false, true); + emit Deposit(user1, depositAmount); + vm.expectEmit(true, false, false, true); + emit UpdateRequestMessage(seqBefore, transfers.encryptedState(), requests); + + vm.prank(user1); + transfers.deposit(depositAmount); + + // Veryify it worked as intended + Transfers.Request memory request = transfers.getRequest(0); + assertEq(balanceBefore + depositAmount, token.balanceOf(address(transfers))); + // assertEq(request.action, Transfers.Action.DEPOSIT); // TODO - fix + assertEq(request.user, user1); + assertEq(request.amount, depositAmount); + assertEq(request.ciphertext, bytes32(0)); + assertEq(seqBefore + 1, transfers.sequenceNum()); + + + } + + function testWithdraw() public { + uint256 seqBefore = transfers.sequenceNum(); + uint256 balanceBefore = token.balanceOf(address(transfers)); + + Transfers.Request[] memory requests = new Transfers.Request[](1); + requests[0] = Transfers.Request(Transfers.Action.WITHDRAW, user1, 0, bytes32(0)); + + vm.expectEmit(true, false, false, false); + emit WithdrawRequest(user1); + vm.expectEmit(true, false, false, true); + emit UpdateRequestMessage(seqBefore, transfers.encryptedState(), requests); + + // User makes a withdrawal request + vm.prank(user1); + transfers.withdraw(); + + Transfers.Request memory request = transfers.getRequest(0); + assertEq(balanceBefore, token.balanceOf(address(transfers))); // Balance shouldn't change yet + // assertEq(request.requestType, Transfers.RequestType.WITHDRAW); // TODO - fix + assertEq(request.user, user1); + assertEq(request.amount, 0); + assertEq(request.ciphertext, bytes32(0)); + assertEq(seqBefore + 1, transfers.sequenceNum()); +} + + + function testTransferRequest() public { + bytes32 ciphertext = keccak256(abi.encodePacked("Encrypted transfer data")); + uint256 seqBefore = transfers.sequenceNum(); + + Transfers.Request[] memory requests = new Transfers.Request[](1); + requests[0] = Transfers.Request(Transfers.Action.TRANSFER, user1, 0, ciphertext); + + vm.expectEmit(true, false, false, true); + emit TransferRequest(user1, ciphertext); + vm.expectEmit(true, false, false, true); + emit UpdateRequestMessage(seqBefore, transfers.encryptedState(), requests); + + // User makes a transfer request + vm.prank(user1); + transfers.transferRequest(ciphertext); + + // Verify that the request was stored correctly + Transfers.Request memory request = transfers.getRequest(0); + assertEq(request.user, user1); + // assertEq(request.requestType, Transfers.RequestType.TRANSFER); // TODO - fix + assertEq(request.amount, 0); + assertEq(request.ciphertext, ciphertext); + assertEq(seqBefore + 1, transfers.sequenceNum()); + } + + function testUpdate() public { + // Prepare dummy data for the test + bytes memory newEncryptedState = abi.encodePacked("Dummy Encrypted State"); + uint256 amount1 = 10 ether; + uint256 amount2 = 20 ether; + + vm.prank(user1); + token.approve(address(transfers), amount1); + vm.prank(user1); + transfers.deposit(amount1); + vm.prank(user2); + token.approve(address(transfers), amount2); + vm.prank(user2); + transfers.deposit(amount2); + + uint256 user1BalanceBefore = token.balanceOf(user1); + uint256 user2BalanceBefore = token.balanceOf(user2); + + // Set expected events for state update and withdrawal responses + vm.expectEmit(false, false, false, true); + emit StateUpdated(newEncryptedState); + vm.expectEmit(true, false, false, true); + emit WithdrawResponse(user1, amount1); + vm.expectEmit(true, false, false, true); + emit WithdrawResponse(user2, amount2); + + // Call update with the prepared data + address[] memory withdrawalAddresses = new address[](2); + withdrawalAddresses[0] = user1; + withdrawalAddresses[1] = user2; + + uint256[] memory withdrawalAmounts = new uint256[](2); + withdrawalAmounts[0] = amount1; + withdrawalAmounts[1] = amount2; + + transfers.update(newEncryptedState, withdrawalAddresses, withdrawalAmounts, dummyQuote); + + // Verify the contract's encrypted state was updated + assertEq(transfers.encryptedState(), newEncryptedState); + + // Verify the token balance of each user after withdrawal + assertEq(token.balanceOf(user1), user1BalanceBefore + amount1); + assertEq(token.balanceOf(user2), user2BalanceBefore + amount2); + + // Check that all requests were cleared after update + assertEq(transfers.getAllRequests().length, 0); + } +}