Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Quartz Solidity Contract for Handshake with Transfers app #281

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[submodule "crates/evm/hello_foundry/lib/forge-std"]
path = crates/evm/hello_foundry/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "crates/evm/contracts/lib/forge-std"]
path = crates/evm/contracts/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "crates/evm/contracts/lib/automata-dcap-attestation"]
path = crates/evm/contracts/lib/automata-dcap-attestation
url = https://github.com/automata-network/automata-dcap-attestation
2 changes: 2 additions & 0 deletions crates/evm/contracts/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SEPOLIA_PRIV_KEY=0x
RPC_URL_SEPOLIA=https://eth-sepolia.g.alchemy.com/v2/<YOUR_ALCHEMY_API_KEY>
14 changes: 14 additions & 0 deletions crates/evm/contracts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Compiler files
cache/
out/

# Ignores development broadcast logs
!/broadcast
/broadcast/*/31337/
/broadcast/**/dry-run/

# Docs
docs/

# Dotenv file
.env
3 changes: 3 additions & 0 deletions crates/evm/contracts/.gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
32 changes: 32 additions & 0 deletions crates/evm/contracts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Quartz Solidity
A Solidity project for the Quartz contract, utilizing Foundry for testing and deployment. This contract integrates with the DCAP attestation system on the Sepolia testnet, providing secure attestation and session management.

## Prerequisites
Foundry: Install Foundry by running:
```bash
curl -L https://foundry.paradigm.xyz | bash
foundryup
```

Environment Variables: Set up a .env file in the project root with your private key and Sepolia RPC URL (alchemy or any other provider):
```
PRIVATE_KEY=your_private_key_here
RPC_URL_SEPOLIA=https://eth-sepolia.alchemyapi.io/v2/YOUR_ALCHEMY_API_KEY
```

## Testing
Run tests on the Sepolia testnet using Foundry. This command forks the Sepolia network, allowing tests to run against real contract data.
This is so we can easily use the deployed attestation contract on sepolia, and not deploy everything ourselves. It does not require Sepolia ETH, as it forks the blockchain state at that block.

```bash
source .env
forge test --fork-url $RPC_URL_SEPOLIA --fork-block-number 7040108
```

## Project Structure
- src/: Contains the Quartz contract source code.
- script/: Deployment scripts using Foundry’s forge script.
- test/: Test files for Quartz contract functions and features.

## License
This project is licensed under the Apache License 2.0.
11 changes: 11 additions & 0 deletions crates/evm/contracts/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[profile.default]
src = "src"
out = "out"
libs = ["lib"]

[rpc_endpoints]
sepolia = "${RPC_URL_SEPOLIA}"

[profile.sepolia]
url = "${RPC_URL_SEPOLIA}"
private_key = "${SEPOLIA_PRIV_KEY}"
1 change: 1 addition & 0 deletions crates/evm/contracts/lib/automata-dcap-attestation
1 change: 1 addition & 0 deletions crates/evm/contracts/lib/forge-std
Submodule forge-std added at 1eea5b
2 changes: 2 additions & 0 deletions crates/evm/contracts/remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
forge-std=lib/forge-std/src
@automata-dcap=lib/automata-dcap-attestation/contracts
98 changes: 98 additions & 0 deletions crates/evm/contracts/src/Quartz.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;

import "@automata-dcap/interfaces/IAttestation.sol";

// QUESTION - We do not test that mr_enclave matches yet in cosmwasm. Should we do it here?
// QUESTION - I guess for the transfers app, or ping pong, they would actually inherit the Quartz contract? Right?
// this way whenever those functions are called on ping pong, they would increase the sequence number (I assume
// the handshake does not update the sequence number)
// QUESTION - Contract address MIGHT be put into the config? (but I dont think that matters on the solidity side as its available globally in the contract)

contract Quartz {
Config public config;
bytes32 public enclavePubKey;
uint256 public sequenceNum;
IAttestation attest = IAttestation(0x76A3657F2d6c5C66733e9b69ACaDadCd0B68788b); // Sepolia address - can set in constructor for other networks
// NOTE - nonce is no longer needed

struct Config {
bytes32 mrEnclave;
LightClientOpts lightClientOpts;
address pccs; // is both the tcbinfo_contract and dcap_verifier_contract of cosmwasm
}

// TODO Shoaib to figure out real values
struct LightClientOpts {
string chainID;
uint256 trustedHeight;
bytes32 trustedHash;
}
// etc.

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
* @dev On failure the contract will not deploy, and the user will lose the gas. The constructor
* is equivalent to the start of the handshake, and the session create, as the event emitted
* can be passed onto the host to share with the enclave
* @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 as per onlyEnclave()
*/
constructor(Config memory _config, bytes memory _quote) onlyEnclave(_quote) {
config = _config;
emit SessionCreated(address(this));
}

/**
* @notice Sets the session public key after verifying the attestation quote
* @dev This function is equivalent to cosmwasm setting of the session, without the nonce, since
* the nonce is no longer needed
* @param _pubKey The public key to be set for the session, provided by the enclave
* @param _quote The attestation quote to be verified, ensuring that the caller is authorized
* 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 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

/**
* @notice Extracts the revert message from a failed external call's return data
* @param _output The raw return data from a failed external call
* @return The string representing the revert message
*/
function _getRevertMessage(bytes memory _output) internal pure returns (string memory) {
if (_output.length == 0) {
return "Unknown error";
}
assembly {
// Skip the first 4 bytes (error selector)
_output := add(_output, 0x04)
}
return abi.decode(_output, (string));
}
}
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
Loading