-
Notifications
You must be signed in to change notification settings - Fork 284
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
92f4b6e
commit fb1b08f
Showing
7 changed files
with
358 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
8 changes: 8 additions & 0 deletions
8
yarn-project/noir-contracts/src/contracts/token_bridge_contract/Nargo.toml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" } |
170 changes: 170 additions & 0 deletions
170
yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
} |
47 changes: 47 additions & 0 deletions
47
yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/token_interface.nr
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
); | ||
} | ||
} |
44 changes: 44 additions & 0 deletions
44
yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/types.nr
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
} | ||
} | ||
} |
87 changes: 87 additions & 0 deletions
87
yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/util.nr
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |