Skip to content

Commit

Permalink
portal contract
Browse files Browse the repository at this point in the history
  • Loading branch information
rahul-kothari committed Sep 17, 2023
1 parent 92f4b6e commit fb1b08f
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 0 deletions.
1 change: 1 addition & 0 deletions yarn-project/end-to-end/src/cli_docs_sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ SchnorrHardcodedAccountContractAbi
SchnorrSingleKeyAccountContractAbi
TestContractAbi
TokenContractAbi
TokenBridgeContractAbi
UniswapContractAbi
// docs:end:example-contracts
`;
Expand Down
1 change: 1 addition & 0 deletions yarn-project/noir-contracts/Nargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ members = [
"src/contracts/schnorr_single_key_account_contract",
"src/contracts/test_contract",
"src/contracts/token_contract",
"src/contracts/token_bridge_contract",
"src/contracts/uniswap_contract",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "token_bridge_contract"
authors = [""]
compiler_version = "0.1"
type = "contract"

[dependencies]
aztec = { path = "../../../../aztec-nr/aztec" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
mod types;
mod util;
mod token_interface;

// Minimal implementation of the token bridge that can move funds between L1 <> L2.
// The bridge has a corresponding Portal contract on L1 that it is attached to
// And corresponds to a Token on L2 that uses the `AuthWit` accounts pattern.
// Bridge has to be set as a minter on the token before it can be sued

contract TokenBridge {
use dep::aztec::{
context::{Context},
state_vars::{public_state::PublicState},
types::type_serialisation::field_serialisation::{
FieldSerialisationMethods, FIELD_SERIALISED_LEN,
},
oracle::compute_selector::compute_selector,
};

use crate::types::{AztecAddress, EthereumAddress};
use crate::token_interface::Token;
use crate::util::{get_mint_content_hash, get_withdraw_content_hash, compute_secret_hash};

// Storage structure, containing all storage, and specifying what slots they use.
struct Storage {
token: PublicState<Field, 1>,
}

impl Storage {
fn init(context: Context) -> pub Self {
Storage {
token: PublicState::new(
context,
1,
FieldSerialisationMethods,
),
}
}
}

// Constructs the contract.
#[aztec(private)]
fn constructor() {
// Currently not possible to execute public calls from constructor as code not yet available to sequencer.
// let selector = compute_selector("_initialize((Field))");
// let _callStackItem = context.call_public_function(context.this_address(), selector, [context.msg_sender()]);
}

// Consumes a L1->L2 message and calls the token contract to mint the appropriate amount publicly
#[aztec(public)]
fn deposit_public(
amount: Field,
msg_key: Field,
secret: Field,
canceller: EthereumAddress,
) -> Field {
let storage = Storage::init(Context::public(&mut context));

let content_hash = get_mint_content_hash(amount, context.msg_sender(), canceller.address);
// Consume message and emit nullifier
context.consume_l1_to_l2_message(msg_key, content_hash, secret);

// Mint tokens
Token::at(storage.token.read()).mint_public(context, context.msg_sender(), amount);

1
}

// Burns the appropriate amount of tokens and creates a L2 to L1 withdraw message publicly
// Requires `from` to give approval to the bridge to burn tokens on their behalf using witness signatures
#[aztec(public)]
fn withdraw_public(
amount: Field,
recipient: EthereumAddress, // ethereum address to withdraw to
callerOnL1: EthereumAddress, // ethereum address that can call this function on the L1 portal (0x0 if anyone can call)
nonce: Field,
) -> Field {
let storage = Storage::init(Context::public(&mut context));

// Burn tokens
Token::at(storage.token.read()).burn_public(context, context.msg_sender(), amount, nonce);

// Send an L2 to L1 message
let content = get_withdraw_content_hash(amount, recipient.address, callerOnL1.address);
context.message_portal(content);
1
}

// Consumes a L1->L2 message and calls the token contract to mint the appropriate amount in private assets
// User needs to call token.redeem_shield() to get the private assets
#[aztec(private)]
fn deposit_private(
amount: Field,
msg_key: Field, // L1 to L2 message key as derived from the inbox contract
secret: Field,
canceller: EthereumAddress,
) -> Field {
// Consume L1 to L2 message and emit nullifier
let content_hash = get_mint_content_hash(amount, context.msg_sender(), canceller.address);
context.consume_l1_to_l2_message(inputs, msg_key, content_hash, secret);

// Mint tokens on L2
// We hash the secret privately and send the hash in a public call so the secret isn't leaked
// Furthermore, `mint_private` on token is public. So we can an internal public function
// which then calls the token contract
let secret_hash = compute_secret_hash(secret);
context.call_public_function(
context.this_address(),
compute_selector("_call_mint_on_token(Field,Field)"),
[amount, secret_hash],
);

1
}

// Burns the appropriate amount of tokens and creates a L2 to L1 withdraw message privately
// Requires `from` to give approval to the bridge to burn tokens on their behalf using witness signatures
#[aztec(private)]
fn withdraw_private(
token: AztecAddress,
amount: Field,
recipient: EthereumAddress, // ethereum address to withdraw to
callerOnL1: EthereumAddress, // ethereum address that can call this function on the L1 portal (0x0 if anyone can call)
nonce: Field,
) -> Field {
// Burn tokens
Token::at(token.address).burn(&mut context, context.msg_sender(), amount, nonce);

// Send an L2 to L1 message
let content = get_withdraw_content_hash(amount, recipient.address, callerOnL1.address);
context.message_portal(content);

// Assert that user provided token address is same as seen in storage.
context.call_public_function(context.this_address(), compute_selector("_assert_token_is_same(Field)"), [token.address]);

1
}

// /// Unconstrained ///

unconstrained fn token() -> Field {
let storage = Storage::init(Context::none());
storage.token.read()
}

/// SHOULD BE Internal ///
// We cannot do this from the constructor currently
// Since this should be internal, for now, we ignore the safety checks of it, as they are
// enforced by it being internal and only called from the constructor.
#[aztec(public)]
fn _initialize(token: AztecAddress) {
let storage = Storage::init(Context::public(&mut context));
storage.token.write(token.address);
}

// This way, user hashes their secret in private and only sends the hash in public
// meaning only user can `redeem_shield` at a later time with their secret.
#[aztec(public)]
internal fn _call_mint_on_token(amount: Field, secret_hash: Field){
let storage = Storage::init(Context::public(&mut context));
Token::at(storage.token.read()).mint_private(context, amount, secret_hash);
}

#[aztec(public)]
internal fn _assert_token_is_same(token: Field) {
let storage = Storage::init(Context::public(&mut context));
assert(storage.token.read() == token, "Token address is not the same as seen in storage");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use dep::aztec::{
context::{ PrivateContext, PublicContext, Context },
oracle::compute_selector::compute_selector,
};

struct Token {
address: Field,
}

impl Token {
fn at(address: Field) -> Self {
Self { address }
}

fn mint_public(self: Self, context: PublicContext, to: Field, amount: Field) {
let _return_values = context.call_public_function(
self.address,
compute_selector("mint_public((Field),Field)"),
[to, amount]
);
}

fn burn_public(self: Self, context: PublicContext, from: Field, amount: Field, nonce: Field) {
let _return_values = context.call_public_function(
self.address,
compute_selector("burn_public((Field),Field,Field)"),
[from, amount, nonce]
);
}

// Private
fn mint_private(self: Self, context: PublicContext, amount: Field, secret_hash: Field) {
let _return_values = context.call_public_function(
self.address,
compute_selector("mint_private(Field,Field)"),
[amount, secret_hash]
);
}

fn burn(self: Self, context: &mut PrivateContext, from: Field, amount: Field, nonce: Field) {
let _return_values = context.call_private_function(
self.address,
compute_selector("burn((Field),Field,Field)"),
[from, amount, nonce]
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
struct AztecAddress {
address: Field
}

impl AztecAddress {
fn new(address: Field) -> Self {
Self {
address
}
}

fn serialize(self: Self) -> [Field; 1] {
[self.address]
}

fn deserialize(fields: [Field; 1]) -> Self {
Self {
address: fields[0]
}
}
}

struct EthereumAddress {
address: Field
}

impl EthereumAddress {
fn new(address: Field) -> Self {
Self {
address
}
}


fn serialize(self: Self) -> [Field; 1] {
[self.address]
}

fn deserialize(fields: [Field; 1]) -> Self {
Self {
address: fields[0]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use dep::std::hash::{pedersen_with_separator, sha256};
use dep::aztec::constants_gen::{
GENERATOR_INDEX__SIGNATURE_PAYLOAD,
GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET,
};

fn compute_secret_hash(secret: Field) -> Field {
// TODO(#1205) This is probably not the right index to use
pedersen_with_separator([secret], GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET)[0]
}

// Computes a content hash of a deposit/mint message.
fn get_mint_content_hash(amount: Field, owner_address: Field, canceller: Field) -> Field {
let mut hash_bytes: [u8; 100] = [0; 100];
let amount_bytes = amount.to_be_bytes(32);
let recipient_bytes = owner_address.to_be_bytes(32);
let canceller_bytes = canceller.to_be_bytes(32);

for i in 0..32 {
hash_bytes[i + 4] = amount_bytes[i];
hash_bytes[i + 36] = recipient_bytes[i];
hash_bytes[i + 68] = canceller_bytes[i];
}

// Function selector: 0xeeb73071 keccak256('mint(uint256,bytes32,address)')
hash_bytes[0] = 0xee;
hash_bytes[1] = 0xb7;
hash_bytes[2] = 0x30;
hash_bytes[3] = 0x71;

let content_sha256 = sha256(hash_bytes);

// // Convert the content_sha256 to a field element
let mut v = 1;
let mut high = 0 as Field;
let mut low = 0 as Field;

for i in 0..16 {
high = high + (content_sha256[15 - i] as Field) * v;
low = low + (content_sha256[16 + 15 - i] as Field) * v;
v = v * 256;
}

// Abuse that a % p + b % p = (a + b) % p and that low < p
let content_hash = low + high * v;
content_hash
}

// Computes a content hash of a withdraw message.
fn get_withdraw_content_hash(amount: Field, recipient: Field, callerOnL1: Field) -> Field {
// Compute the content hash
// Compute sha256(selector || amount || recipient)
// then convert to a single field element
// add that to the l2 to l1 messages
let mut hash_bytes: [u8; 100] = [0; 100];
let amount_bytes = amount.to_be_bytes(32);
let recipient_bytes = recipient.to_be_bytes(32);
let callerOnL1_bytes = callerOnL1.to_be_bytes(32);

// 0xb460af94, selector for "withdraw(uint256,address,address)"
hash_bytes[0] = 0xb4;
hash_bytes[1] = 0x60;
hash_bytes[2] = 0xaf;
hash_bytes[3] = 0x94;

for i in 0..32 {
hash_bytes[i + 4] = amount_bytes[i];
hash_bytes[i + 36] = recipient_bytes[i];
hash_bytes[i + 68] = callerOnL1_bytes[i];
}
let content_sha256 = sha256(hash_bytes);

// Convert the content_sha256 to a field element
let mut v = 1;
let mut high = 0 as Field;
let mut low = 0 as Field;

for i in 0..16 {
high = high + (content_sha256[15 - i] as Field) * v;
low = low + (content_sha256[16 + 15 - i] as Field) * v;
v = v * 256;
}

// Abuse that a % p + b % p = (a + b) % p and that low < p
let content = low + high * v;
content
}

0 comments on commit fb1b08f

Please sign in to comment.