Skip to content

Commit

Permalink
Add in transfers app (#282)
Browse files Browse the repository at this point in the history
Co-authored-by: Dave Kaj <[email protected]>
  • Loading branch information
davekaj and Dave Kaj authored Nov 14, 2024
1 parent 65e0497 commit e67da48
Show file tree
Hide file tree
Showing 12 changed files with 1,114 additions and 66 deletions.
45 changes: 0 additions & 45 deletions crates/evm/contracts/.github/workflows/test.yml

This file was deleted.

40 changes: 21 additions & 19 deletions crates/evm/contracts/src/Quartz.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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));
}

/**
Expand All @@ -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
Expand Down
175 changes: 175 additions & 0 deletions crates/evm/contracts/src/Transfers.sol
Original file line number Diff line number Diff line change
@@ -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<Request> 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;
}
}
28 changes: 28 additions & 0 deletions crates/evm/contracts/src/openzeppelin/Context.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit e67da48

Please sign in to comment.