diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..882ec525 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "examples/bridge/l1/lib/forge-std"] + path = examples/bridge/l1/lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "examples/bridge/l1/lib/create3-factory"] + path = examples/bridge/l1/lib/create3-factory + url = https://github.com/zeframlou/create3-factory diff --git a/Scarb.toml b/Scarb.toml index 23022e71..c8417af3 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -1,5 +1,12 @@ [workspace] -members = ["crates", "examples/*", "token"] +members = [ + "crates", + "examples/chess", + "examples/hex_map", + "examples/market", + "examples/projectile", + "token", +] [workspace.package] version = "0.5.0" diff --git a/examples/bridge/DEPLOYMENT.md b/examples/bridge/DEPLOYMENT.md new file mode 100644 index 00000000..f6e6561f --- /dev/null +++ b/examples/bridge/DEPLOYMENT.md @@ -0,0 +1,41 @@ +in /l1 & /sn Makefile, import the right .env +use `make` without argument to check current env vars + +## L1 + +- set STARKNET_ADDRESS (for goerli & mainnet) +- set TOKEN_ADDRESS (for mainnet) +- Pre-compute L1DojoBridge address with create3 + +## Starknet + +- set L1_BRIDGE_ADDRESS in sn/ .env +- Deploy contracts +- Set auth +- Initialize contracts + +## L1 + +- set L2_BRIDGE_ADDRESS in l1/ .env +- Deploy L1DojoBridge + + + +## Constructor / initiliazers + +### L1 +L1DojoBridge constructor : + +`constructor(address _starknet, address _l1Token, uint256 _l2Bridge)` + +### SN + +dojo_bridge initializer : + +`fn initializer(ref self: ContractState, l1_bridge: felt252, l2_token: ContractAddress)` + +dojo_token initializer : + +`fn initializer(ref self: ContractState, name: felt252, symbol: felt252, l2_bridge_address: ContractAddress)` + + diff --git a/examples/bridge/DEVELOPMENT.md b/examples/bridge/DEVELOPMENT.md new file mode 100644 index 00000000..94aa9082 --- /dev/null +++ b/examples/bridge/DEVELOPMENT.md @@ -0,0 +1,89 @@ +For local use +Make sure both Makefiles use .env.local +You can just use `make` without argument to check current env vars + +# ETH + +### Terminal 1 + +Launch anvil + +```sh +cd l1 +make anvil +``` + +### Terminal 2 + +Deploy create3 contract for deterministic contract address +```sh +cd l1 +make create3 +``` + +Deploy eth $TOKEN & L1DojoBridge +```sh +make deploy +``` + +# Starknet + +### Terminal 1 + +Launch katana with messaging + +```sh +cd sn +make katana_msg +# or +katana --messaging anvil.messaging.json +``` + +### Terminal 2 + +Migrate & initialize contracts +```sh +cd sn +make migrate_and_init +# or +make migrate +make initialize +``` + +Fund an address in ETH +```sh +./scripts/fund.sh 0x1234 +``` + +# Bridging + +## from ETH to Starknet + +it mint tokens & approve bridge & call deposit on ETH bridge +```sh +make deposit +``` + +## from Starknet to ETH + +get balance on Starknet +```sh +scarb run get_balance +``` + +withdraw from Starknet to ETH +```sh +scarb run withdraw +``` + +## after + +withdraw tokens from ETH bridge +```sh +make withdraw +``` + +check token balance on ETH +```sh +make get_balance +``` \ No newline at end of file diff --git a/examples/bridge/README.md b/examples/bridge/README.md new file mode 100644 index 00000000..ff0b6652 --- /dev/null +++ b/examples/bridge/README.md @@ -0,0 +1,14 @@ +## DOJO BRIDGE + +Adaptation of https://github.com/BibliothecaDAO/Starknet-ERC20-bridge & https://github.com/glihm/starknet-messaging-dev for Dojo. + + +## Requirements + +Please before starting, install: + +- [scarb](https://docs.swmansion.com/scarb/) to build cairo contracts. +- [starkli](https://github.com/xJonathanLEI/starkli) to interact with Katana. +- [foundry](https://book.getfoundry.sh/getting-started/installation) to interact with Anvil. +- git submodules forge-std & create3-factory. + diff --git a/examples/bridge/l1/.env.local b/examples/bridge/l1/.env.local new file mode 100644 index 00000000..c4a405f5 --- /dev/null +++ b/examples/bridge/l1/.env.local @@ -0,0 +1,13 @@ +ENV=local + +ETH_RPC_URL=http://127.0.0.1:8545 + +ACCOUNT_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +ACCOUNT_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 + +# local +CREATE3_FACTORY_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3 +STARKNET_ADDRESS=0x0000000000000000000000000000000000000000 +TOKEN_ADDRESS=0x0000000000000000000000000000000000000000 + +L2_BRIDGE_ADDRESS=0x2ff2f9994ba7e039f50190cb3b3dc538d9abf7201acbe5a6a7aff686dd40d89 diff --git a/examples/bridge/l1/.gitignore b/examples/bridge/l1/.gitignore new file mode 100644 index 00000000..cbd1128e --- /dev/null +++ b/examples/bridge/l1/.gitignore @@ -0,0 +1,14 @@ +# Dotenv file +.env + +# Compiler files +cache/ +out/ +broadcast/ + + +lib/forge-std + +/logs + +.gas-snapshot \ No newline at end of file diff --git a/examples/bridge/l1/Makefile b/examples/bridge/l1/Makefile new file mode 100644 index 00000000..7c25d69a --- /dev/null +++ b/examples/bridge/l1/Makefile @@ -0,0 +1,44 @@ +include .env.local + +export + +PARAMS := --broadcast --rpc-url ${ETH_RPC_URL} -v + +all: + @echo "********************************************************************" + @echo "ENV : ${ENV}" + @echo "********************************************************************" + @echo "ETH_RPC_URL : ${ETH_RPC_URL}" + @echo "ACCOUNT_ADDRESS : ${ACCOUNT_ADDRESS}" + @echo "CREATE3_FACTORY_ADDRESS : ${CREATE3_FACTORY_ADDRESS}" + @echo "STARKNET_ADDRESS : ${STARKNET_ADDRESS}" + @echo "TOKEN_ADDRESSS : ${TOKEN_ADDRESSS}" + @echo "L2_BRIDGE_ADDRESS : ${L2_BRIDGE_ADDRESS}" + @echo "********************************************************************" + +anvil: + anvil --chain-id 1337 + +anvil_slow: + anvil --chain-id 1337 --block-time 10 + +create3: + forge script ./script/Deploy.s.sol:Create3 ${PARAMS} + +get_bridge_address: + forge script ./script/Deploy.s.sol:GetBridgeAddress ${PARAMS} + +deploy: + forge script ./script/Deploy.s.sol:Deploy ${PARAMS} + +deposit: + forge script ./script/Calls.s.sol:Deposit ${PARAMS} + +withdraw: + forge script ./script/Calls.s.sol:Withdraw ${PARAMS} + +get_balance: + forge script ./script/Calls.s.sol:GetBalance ${PARAMS} + +mint_token: + forge script ./script/Calls.s.sol:MintToken ${PARAMS} \ No newline at end of file diff --git a/examples/bridge/l1/foundry.toml b/examples/bridge/l1/foundry.toml new file mode 100644 index 00000000..fbe80ee9 --- /dev/null +++ b/examples/bridge/l1/foundry.toml @@ -0,0 +1,20 @@ +[profile.default] +solc-version = "0.8.13" +src = "src" +out = "out" +libs = ["lib"] +fs_permissions = [{ access = "read-write", path = "./logs"}] +remappings = [ + "@forge-std/=lib/forge-std/src/", + "@create3-factory/=lib/create3-factory/src/", + "@openzeppelin/=lib/@openzeppelin/", + "@starknet/=lib/starknet/", +] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options + + +# since the foundry.toml is commited it's not recommened to have the api key explicitly +# [rpc_endpoints] +# local = "${ETH_RPC_URL}" + diff --git a/examples/bridge/l1/lib/@openzeppelin/contracts/access/Ownable.sol b/examples/bridge/l1/lib/@openzeppelin/contracts/access/Ownable.sol new file mode 100644 index 00000000..16469d5a --- /dev/null +++ b/examples/bridge/l1/lib/@openzeppelin/contracts/access/Ownable.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../utils/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. + * + * By default, the owner account will be the one that deploys the contract. 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; + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /** + * @dev Initializes the contract setting the deployer as the initial owner. + */ + constructor() { + _setOwner(_msgSender()); + } + + /** + * @dev Returns the address of the current owner. + */ + function owner() public view virtual returns (address) { + return _owner; + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + require(owner() == _msgSender(), "Ownable: caller is not the owner"); + _; + } + + /** + * @dev Leaves the contract without owner. It will not be possible to call + * `onlyOwner` functions anymore. Can only be called by the current owner. + * + * NOTE: Renouncing ownership will leave the contract without an owner, + * thereby removing any functionality that is only available to the owner. + */ + function renounceOwnership() public virtual onlyOwner { + _setOwner(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 { + require(newOwner != address(0), "Ownable: new owner is the zero address"); + _setOwner(newOwner); + } + + function _setOwner(address newOwner) private { + address oldOwner = _owner; + _owner = newOwner; + emit OwnershipTransferred(oldOwner, newOwner); + } +} diff --git a/examples/bridge/l1/lib/@openzeppelin/contracts/token/ERC20/ERC20.sol b/examples/bridge/l1/lib/@openzeppelin/contracts/token/ERC20/ERC20.sol new file mode 100644 index 00000000..46122eb0 --- /dev/null +++ b/examples/bridge/l1/lib/@openzeppelin/contracts/token/ERC20/ERC20.sol @@ -0,0 +1,355 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./IERC20.sol"; +import "./extensions/IERC20Metadata.sol"; +import "../../utils/Context.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}. + * For a generic mechanism see {ERC20PresetMinterPauser}. + * + * TIP: For a detailed writeup see our guide + * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * 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 ERC20 + * applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {IERC20-approve}. + */ +contract ERC20 is Context, IERC20, IERC20Metadata { + mapping(address => uint256) private _balances; + + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + + /** + * @dev Sets the values for {name} and {symbol}. + * + * The default value of {decimals} is 18. To select a different value for + * {decimals} you should overload it. + * + * 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 override returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual override 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 value {ERC20} uses, unless this function is + * 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 override returns (uint8) { + return 18; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view virtual override returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view virtual override returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `recipient` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address recipient, uint256 amount) public virtual override returns (bool) { + _transfer(_msgSender(), recipient, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) public virtual override returns (bool) { + _approve(_msgSender(), spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * Requirements: + * + * - `sender` and `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + * - the caller must have allowance for ``sender``'s tokens of at least + * `amount`. + */ + function transferFrom( + address sender, + address recipient, + uint256 amount + ) public virtual override returns (bool) { + _transfer(sender, recipient, amount); + + uint256 currentAllowance = _allowances[sender][_msgSender()]; + require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance"); + unchecked { + _approve(sender, _msgSender(), currentAllowance - amount); + } + + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender] + addedValue); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + uint256 currentAllowance = _allowances[_msgSender()][spender]; + require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero"); + unchecked { + _approve(_msgSender(), spender, currentAllowance - subtractedValue); + } + + return true; + } + + /** + * @dev Moves `amount` of tokens from `sender` to `recipient`. + * + * 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. + * + * Requirements: + * + * - `sender` cannot be the zero address. + * - `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + */ + function _transfer( + address sender, + address recipient, + uint256 amount + ) internal virtual { + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(sender, recipient, amount); + + uint256 senderBalance = _balances[sender]; + require(senderBalance >= amount, "ERC20: transfer amount exceeds balance"); + unchecked { + _balances[sender] = senderBalance - amount; + } + _balances[recipient] += amount; + + emit Transfer(sender, recipient, amount); + + _afterTokenTransfer(sender, recipient, amount); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply += amount; + _balances[account] += amount; + emit Transfer(address(0), account, amount); + + _afterTokenTransfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + uint256 accountBalance = _balances[account]; + require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); + unchecked { + _balances[account] = accountBalance - amount; + } + _totalSupply -= amount; + + emit Transfer(account, address(0), amount); + + _afterTokenTransfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` 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. + */ + function _approve( + address owner, + address spender, + uint256 amount + ) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Hook that is called before any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * will be transferred to `to`. + * - when `from` is zero, `amount` tokens will be minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} + + /** + * @dev Hook that is called after any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * has been transferred to `to`. + * - when `from` is zero, `amount` tokens have been minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens have been burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} +} diff --git a/examples/bridge/l1/lib/@openzeppelin/contracts/token/ERC20/IERC20.sol b/examples/bridge/l1/lib/@openzeppelin/contracts/token/ERC20/IERC20.sol new file mode 100644 index 00000000..08a04ad7 --- /dev/null +++ b/examples/bridge/l1/lib/@openzeppelin/contracts/token/ERC20/IERC20.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 amount) 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 `amount` 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 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `sender` to `recipient` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool); + + /** + * @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); +} diff --git a/examples/bridge/l1/lib/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol b/examples/bridge/l1/lib/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol new file mode 100644 index 00000000..4fb868ae --- /dev/null +++ b/examples/bridge/l1/lib/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../IERC20.sol"; + +/** + * @dev Interface for the optional metadata functions from the ERC20 standard. + * + * _Available since v4.1._ + */ +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); +} diff --git a/examples/bridge/l1/lib/@openzeppelin/contracts/utils/Context.sol b/examples/bridge/l1/lib/@openzeppelin/contracts/utils/Context.sol new file mode 100644 index 00000000..d03dc5f4 --- /dev/null +++ b/examples/bridge/l1/lib/@openzeppelin/contracts/utils/Context.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @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; + } +} diff --git a/examples/bridge/l1/lib/create3-factory b/examples/bridge/l1/lib/create3-factory new file mode 160000 index 00000000..0a42c507 --- /dev/null +++ b/examples/bridge/l1/lib/create3-factory @@ -0,0 +1 @@ +Subproject commit 0a42c5072ce05a19c8fc5d875e52963c5f16849e diff --git a/examples/bridge/l1/lib/starknet/IStarknetMessaging.sol b/examples/bridge/l1/lib/starknet/IStarknetMessaging.sol new file mode 100644 index 00000000..b3ebea4b --- /dev/null +++ b/examples/bridge/l1/lib/starknet/IStarknetMessaging.sol @@ -0,0 +1,76 @@ +/* + Copyright 2019-2022 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.0; + +import "./IStarknetMessagingEvents.sol"; + +interface IStarknetMessaging is IStarknetMessagingEvents { + /** + Returns the max fee (in Wei) that StarkNet will accept per single message. + */ + function getMaxL1MsgFee() external pure returns (uint256); + + /** + Sends a message to an L2 contract. + This function is payable, the payed amount is the message fee. + + Returns the hash of the message and the nonce of the message. + */ + function sendMessageToL2( + uint256 toAddress, + uint256 selector, + uint256[] calldata payload + ) external payable returns (bytes32, uint256); + + /** + Consumes a message that was sent from an L2 contract. + + Returns the hash of the message. + */ + function consumeMessageFromL2(uint256 fromAddress, uint256[] calldata payload) + external + returns (bytes32); + + /** + Starts the cancellation of an L1 to L2 message. + A message can be canceled messageCancellationDelay() seconds after this function is called. + + Note: This function may only be called for a message that is currently pending and the caller + must be the sender of the that message. + */ + function startL1ToL2MessageCancellation( + uint256 toAddress, + uint256 selector, + uint256[] calldata payload, + uint256 nonce + ) external returns (bytes32); + + /** + Cancels an L1 to L2 message, this function should be called at least + messageCancellationDelay() seconds after the call to startL1ToL2MessageCancellation(). + A message may only be cancelled by its sender. + If the message is missing, the call will revert. + + Note that the message fee is not refunded. + */ + function cancelL1ToL2Message( + uint256 toAddress, + uint256 selector, + uint256[] calldata payload, + uint256 nonce + ) external returns (bytes32); +} diff --git a/examples/bridge/l1/lib/starknet/IStarknetMessagingEvents.sol b/examples/bridge/l1/lib/starknet/IStarknetMessagingEvents.sol new file mode 100644 index 00000000..11937727 --- /dev/null +++ b/examples/bridge/l1/lib/starknet/IStarknetMessagingEvents.sol @@ -0,0 +1,66 @@ +/* + Copyright 2019-2022 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.0; + +interface IStarknetMessagingEvents { + // This event needs to be compatible with the one defined in Output.sol. + event LogMessageToL1(uint256 indexed fromAddress, address indexed toAddress, uint256[] payload); + + // An event that is raised when a message is sent from L1 to L2. + event LogMessageToL2( + address indexed fromAddress, + uint256 indexed toAddress, + uint256 indexed selector, + uint256[] payload, + uint256 nonce, + uint256 fee + ); + + // An event that is raised when a message from L2 to L1 is consumed. + event ConsumedMessageToL1( + uint256 indexed fromAddress, + address indexed toAddress, + uint256[] payload + ); + + // An event that is raised when a message from L1 to L2 is consumed. + event ConsumedMessageToL2( + address indexed fromAddress, + uint256 indexed toAddress, + uint256 indexed selector, + uint256[] payload, + uint256 nonce + ); + + // An event that is raised when a message from L1 to L2 Cancellation is started. + event MessageToL2CancellationStarted( + address indexed fromAddress, + uint256 indexed toAddress, + uint256 indexed selector, + uint256[] payload, + uint256 nonce + ); + + // An event that is raised when a message from L1 to L2 is canceled. + event MessageToL2Canceled( + address indexed fromAddress, + uint256 indexed toAddress, + uint256 indexed selector, + uint256[] payload, + uint256 nonce + ); +} diff --git a/examples/bridge/l1/lib/starknet/NamedStorage.sol b/examples/bridge/l1/lib/starknet/NamedStorage.sol new file mode 100644 index 00000000..5279f380 --- /dev/null +++ b/examples/bridge/l1/lib/starknet/NamedStorage.sol @@ -0,0 +1,120 @@ +/* + Copyright 2019-2022 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.0; + +/* + Library to provide basic storage, in storage location out of the low linear address space. + + New types of storage variables should be added here upon need. +*/ +library NamedStorage { + function bytes32ToUint256Mapping(string memory tag_) + internal + pure + returns (mapping(bytes32 => uint256) storage randomVariable) + { + bytes32 location = keccak256(abi.encodePacked(tag_)); + assembly { + randomVariable.slot := location + } + } + + function bytes32ToAddressMapping(string memory tag_) + internal + pure + returns (mapping(bytes32 => address) storage randomVariable) + { + bytes32 location = keccak256(abi.encodePacked(tag_)); + assembly { + randomVariable.slot := location + } + } + + function uintToAddressMapping(string memory tag_) + internal + pure + returns (mapping(uint256 => address) storage randomVariable) + { + bytes32 location = keccak256(abi.encodePacked(tag_)); + assembly { + randomVariable.slot := location + } + } + + function addressToBoolMapping(string memory tag_) + internal + pure + returns (mapping(address => bool) storage randomVariable) + { + bytes32 location = keccak256(abi.encodePacked(tag_)); + assembly { + randomVariable.slot := location + } + } + + function getUintValue(string memory tag_) internal view returns (uint256 retVal) { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + retVal := sload(slot) + } + } + + function setUintValue(string memory tag_, uint256 value) internal { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + sstore(slot, value) + } + } + + function setUintValueOnce(string memory tag_, uint256 value) internal { + require(getUintValue(tag_) == 0, "ALREADY_SET"); + setUintValue(tag_, value); + } + + function getAddressValue(string memory tag_) internal view returns (address retVal) { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + retVal := sload(slot) + } + } + + function setAddressValue(string memory tag_, address value) internal { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + sstore(slot, value) + } + } + + function setAddressValueOnce(string memory tag_, address value) internal { + require(getAddressValue(tag_) == address(0x0), "ALREADY_SET"); + setAddressValue(tag_, value); + } + + function getBoolValue(string memory tag_) internal view returns (bool retVal) { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + retVal := sload(slot) + } + } + + function setBoolValue(string memory tag_, bool value) internal { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + sstore(slot, value) + } + } +} diff --git a/examples/bridge/l1/lib/starknet/StarknetMessaging.sol b/examples/bridge/l1/lib/starknet/StarknetMessaging.sol new file mode 100644 index 00000000..c8a1a63c --- /dev/null +++ b/examples/bridge/l1/lib/starknet/StarknetMessaging.sol @@ -0,0 +1,202 @@ +/* + Copyright 2019-2022 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.0; + +import "./IStarknetMessaging.sol"; +import "./NamedStorage.sol"; + +/** + Implements sending messages to L2 by adding them to a pipe and consuming messages from L2 by + removing them from a different pipe. A deriving contract can handle the former pipe and add items + to the latter pipe while interacting with L2. +*/ +contract StarknetMessaging is IStarknetMessaging { + /* + Random slot storage elements and accessors. + */ + string constant L1L2_MESSAGE_MAP_TAG = "STARKNET_1.0_MSGING_L1TOL2_MAPPPING_V2"; + string constant L2L1_MESSAGE_MAP_TAG = "STARKNET_1.0_MSGING_L2TOL1_MAPPPING"; + + string constant L1L2_MESSAGE_NONCE_TAG = "STARKNET_1.0_MSGING_L1TOL2_NONCE"; + + string constant L1L2_MESSAGE_CANCELLATION_MAP_TAG = ( + "STARKNET_1.0_MSGING_L1TOL2_CANCELLATION_MAPPPING" + ); + + string constant L1L2_MESSAGE_CANCELLATION_DELAY_TAG = ( + "STARKNET_1.0_MSGING_L1TOL2_CANCELLATION_DELAY" + ); + + uint256 constant MAX_L1_MSG_FEE = 1 ether; + + function getMaxL1MsgFee() public pure override returns (uint256) { + return MAX_L1_MSG_FEE; + } + + /** + Returns the msg_fee + 1 for the message with the given 'msgHash', + or 0 if no message with such a hash is pending. + */ + function l1ToL2Messages(bytes32 msgHash) external view returns (uint256) { + return l1ToL2Messages()[msgHash]; + } + + function l2ToL1Messages(bytes32 msgHash) external view returns (uint256) { + return l2ToL1Messages()[msgHash]; + } + + function l1ToL2Messages() internal pure returns (mapping(bytes32 => uint256) storage) { + return NamedStorage.bytes32ToUint256Mapping(L1L2_MESSAGE_MAP_TAG); + } + + function l2ToL1Messages() internal pure returns (mapping(bytes32 => uint256) storage) { + return NamedStorage.bytes32ToUint256Mapping(L2L1_MESSAGE_MAP_TAG); + } + + function l1ToL2MessageNonce() public view returns (uint256) { + return NamedStorage.getUintValue(L1L2_MESSAGE_NONCE_TAG); + } + + function messageCancellationDelay() public view returns (uint256) { + return NamedStorage.getUintValue(L1L2_MESSAGE_CANCELLATION_DELAY_TAG); + } + + function messageCancellationDelay(uint256 delayInSeconds) internal { + NamedStorage.setUintValue(L1L2_MESSAGE_CANCELLATION_DELAY_TAG, delayInSeconds); + } + + /** + Returns the timestamp at the time cancelL1ToL2Message was called with a message + matching 'msgHash'. + + The function returns 0 if cancelL1ToL2Message was never called. + */ + function l1ToL2MessageCancellations(bytes32 msgHash) external view returns (uint256) { + return l1ToL2MessageCancellations()[msgHash]; + } + + function l1ToL2MessageCancellations() + internal + pure + returns (mapping(bytes32 => uint256) storage) + { + return NamedStorage.bytes32ToUint256Mapping(L1L2_MESSAGE_CANCELLATION_MAP_TAG); + } + + /** + Returns the hash of an L1 -> L2 message from msg.sender. + */ + function getL1ToL2MsgHash( + uint256 toAddress, + uint256 selector, + uint256[] calldata payload, + uint256 nonce + ) internal view returns (bytes32) { + return + keccak256( + abi.encodePacked( + // MODIFIED HERE: adding uint160 for casting. + uint256(uint160(msg.sender)), + toAddress, + nonce, + selector, + payload.length, + payload + ) + ); + } + + /** + Sends a message to an L2 contract. + */ + function sendMessageToL2( + uint256 toAddress, + uint256 selector, + uint256[] calldata payload + ) external payable override returns (bytes32, uint256) { + require(msg.value > 0, "L1_MSG_FEE_MUST_BE_GREATER_THAN_0"); + require(msg.value <= getMaxL1MsgFee(), "MAX_L1_MSG_FEE_EXCEEDED"); + uint256 nonce = l1ToL2MessageNonce(); + NamedStorage.setUintValue(L1L2_MESSAGE_NONCE_TAG, nonce + 1); + emit LogMessageToL2(msg.sender, toAddress, selector, payload, nonce, msg.value); + bytes32 msgHash = getL1ToL2MsgHash(toAddress, selector, payload, nonce); + // Note that the inclusion of the unique nonce in the message hash implies that + // l1ToL2Messages()[msgHash] was not accessed before. + l1ToL2Messages()[msgHash] = msg.value + 1; + return (msgHash, nonce); + } + + /** + Consumes a message that was sent from an L2 contract. + + Returns the hash of the message. + */ + function consumeMessageFromL2(uint256 fromAddress, uint256[] calldata payload) + external + override + returns (bytes32) + { + bytes32 msgHash = keccak256( + // MODIFIED HERE: adding uint160 for casting. + abi.encodePacked(fromAddress, uint256(uint160(msg.sender)), payload.length, payload) + ); + + require(l2ToL1Messages()[msgHash] > 0, "INVALID_MESSAGE_TO_CONSUME"); + emit ConsumedMessageToL1(fromAddress, msg.sender, payload); + l2ToL1Messages()[msgHash] -= 1; + return msgHash; + } + + function startL1ToL2MessageCancellation( + uint256 toAddress, + uint256 selector, + uint256[] calldata payload, + uint256 nonce + ) external override returns (bytes32) { + emit MessageToL2CancellationStarted(msg.sender, toAddress, selector, payload, nonce); + bytes32 msgHash = getL1ToL2MsgHash(toAddress, selector, payload, nonce); + uint256 msgFeePlusOne = l1ToL2Messages()[msgHash]; + require(msgFeePlusOne > 0, "NO_MESSAGE_TO_CANCEL"); + l1ToL2MessageCancellations()[msgHash] = block.timestamp; + return msgHash; + } + + function cancelL1ToL2Message( + uint256 toAddress, + uint256 selector, + uint256[] calldata payload, + uint256 nonce + ) external override returns (bytes32) { + emit MessageToL2Canceled(msg.sender, toAddress, selector, payload, nonce); + // Note that the message hash depends on msg.sender, which prevents one contract from + // cancelling another contract's message. + // Trying to do so will result in NO_MESSAGE_TO_CANCEL. + bytes32 msgHash = getL1ToL2MsgHash(toAddress, selector, payload, nonce); + uint256 msgFeePlusOne = l1ToL2Messages()[msgHash]; + require(msgFeePlusOne != 0, "NO_MESSAGE_TO_CANCEL"); + + uint256 requestTime = l1ToL2MessageCancellations()[msgHash]; + require(requestTime != 0, "MESSAGE_CANCELLATION_NOT_REQUESTED"); + + uint256 cancelAllowedTime = requestTime + messageCancellationDelay(); + require(cancelAllowedTime >= requestTime, "CANCEL_ALLOWED_TIME_OVERFLOW"); + require(block.timestamp >= cancelAllowedTime, "MESSAGE_CANCELLATION_NOT_ALLOWED_YET"); + + l1ToL2Messages()[msgHash] = 0; + return (msgHash); + } +} diff --git a/examples/bridge/l1/script/BaseScript.sol b/examples/bridge/l1/script/BaseScript.sol new file mode 100644 index 00000000..16b8038f --- /dev/null +++ b/examples/bridge/l1/script/BaseScript.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "forge-std/Script.sol"; +import "@create3-factory/CREATE3Factory.sol"; + +contract BaseScript is Script { + struct Env { + string ENV; + string ETH_RPC_URL; + uint256 ACCOUNT_PRIVATE_KEY; + address ACCOUNT_ADDRESS; + address CREATE3_FACTORY_ADDRESS; + address STARKNET_ADDRESS; + uint256 L2_BRIDGE_ADDRESS; + address TOKEN_ADDRESS; + } + + Env public env; + + constructor() { + loadEnv(); + } + + function getEnv() public view returns (Env memory ) { + return env; + } + + function loadEnv() public { + console.log("Loading Env..."); + + env.ENV = vm.envString("ENV"); + env.ETH_RPC_URL = vm.envString("ETH_RPC_URL"); + env.ACCOUNT_PRIVATE_KEY = vm.envUint("ACCOUNT_PRIVATE_KEY"); + env.ACCOUNT_ADDRESS = vm.envAddress("ACCOUNT_ADDRESS"); + env.CREATE3_FACTORY_ADDRESS = vm.envAddress("CREATE3_FACTORY_ADDRESS"); + env.STARKNET_ADDRESS = vm.envAddress("STARKNET_ADDRESS"); + env.L2_BRIDGE_ADDRESS = vm.envUint("L2_BRIDGE_ADDRESS"); + env.TOKEN_ADDRESS = vm.envAddress("TOKEN_ADDRESS"); + + console.log("ENV :", env.ENV); + console.log("ETH_RPC_URL :", env.ETH_RPC_URL); + + } + + function isLocal() public view returns (bool) { + return stringEquals(env.ENV, "local"); + } + + function isGoerli() public view returns (bool) { + return stringEquals(env.ENV, "goerli"); + } + + function stringEquals(string memory a, string memory b) public pure returns (bool) { + return keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b)); + } +} diff --git a/examples/bridge/l1/script/Calls.s.sol b/examples/bridge/l1/script/Calls.s.sol new file mode 100644 index 00000000..7b279bb2 --- /dev/null +++ b/examples/bridge/l1/script/Calls.s.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "forge-std/Script.sol"; + +import "../src/StarknetMessagingLocal.sol"; +import "../src/Token.sol"; +import "../src/L1DojoBridge.sol"; + +import "./BaseScript.sol"; + + +contract Deposit is BaseScript { + address _token; + address _L1DojoBridge; + + function setUp() public { + string memory json = vm.readFile('./logs/local.json'); + + _token = abi.decode(vm.parseJson(json,'.Token'), (address)); + _L1DojoBridge = abi.decode(vm.parseJson(json,'.L1DojoBridge'), (address)); + } + + function run() public{ + vm.startBroadcast(this.getEnv().ACCOUNT_PRIVATE_KEY); + + uint256 amount = 1_000 * 10**18; + uint256 l2Recipient = 0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973; + uint fee = 30_000; + + // mint token to owner + Token(_token).mint(this.getEnv().ACCOUNT_ADDRESS, amount); + + // owner approve bridge for amount + Token(_token).approve(_L1DojoBridge, amount); + + // call token bridge + L1DojoBridge(_L1DojoBridge).deposit{value: fee}(amount, l2Recipient, fee); + + vm.stopBroadcast(); + } +} + + +contract Withdraw is BaseScript { + address _token; + address _L1DojoBridge; + + function setUp() public { + string memory json = vm.readFile('./logs/local.json'); + + _token = abi.decode(vm.parseJson(json,'.Token'), (address)); + _L1DojoBridge = abi.decode(vm.parseJson(json,'.L1DojoBridge'), (address)); + } + + function run() public returns(uint256 balance){ + vm.startBroadcast(this.getEnv().ACCOUNT_PRIVATE_KEY); + + uint256 amount = 1_000 * 10**18; + address l1Recipient = this.getEnv().ACCOUNT_ADDRESS; + uint256 l2TxHash = 0; + + + // call token bridge + L1DojoBridge(_L1DojoBridge).withdraw(amount, l1Recipient, l2TxHash); + + // get balance + balance = Token(_token).balanceOf(l1Recipient); + + vm.stopBroadcast(); + } +} + +contract GetBalance is BaseScript { + address _token; + + function setUp() public { + string memory json = vm.readFile('./logs/local.json'); + _token = abi.decode(vm.parseJson(json,'.Token'), (address)); + } + + function run() public returns(uint256 balance){ + vm.startBroadcast(this.getEnv().ACCOUNT_PRIVATE_KEY); + + // get balance + balance = Token(_token).balanceOf(this.getEnv().ACCOUNT_ADDRESS); + + vm.stopBroadcast(); + } +} + + + +contract MintToken is BaseScript { + address _token; + + function setUp() public { + string memory json = vm.readFile('./logs/local.json'); + _token = abi.decode(vm.parseJson(json,'.Token'), (address)); + } + + function run() public{ + vm.startBroadcast(this.getEnv().ACCOUNT_PRIVATE_KEY); + + uint256 amount = 1_000 * 10**18; + // mint token to owner + Token(_token).mint(this.getEnv().ACCOUNT_ADDRESS, amount); + + vm.stopBroadcast(); + } +} diff --git a/examples/bridge/l1/script/Deploy.s.sol b/examples/bridge/l1/script/Deploy.s.sol new file mode 100644 index 00000000..d478c948 --- /dev/null +++ b/examples/bridge/l1/script/Deploy.s.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "forge-std/Script.sol"; +import "@create3-factory/CREATE3Factory.sol"; + +import "../src/StarknetMessagingLocal.sol"; +import "../src/Token.sol"; +import "../src/L1DojoBridge.sol"; +import "./BaseScript.sol"; + +bytes32 constant CREATE3_SALT = keccak256(bytes("CREATE3Salt")); + +bytes32 constant SN_SALT = keccak256(bytes("SNSalt")); +bytes32 constant TOKEN_SALT = keccak256(bytes("TokenSalt")); +bytes32 constant BRIDGE_SALT = keccak256(bytes("BridgeSalt")); + + +/** + Deploys the ContractMsg and StarknetMessagingLocal contracts. + Very handy to quickly setup Anvil to debug. +*/ +contract Deploy is BaseScript { + function setUp() public {} + + function run() public { + CREATE3Factory factory = CREATE3Factory( + this.getEnv().CREATE3_FACTORY_ADDRESS + ); + + string memory json = ""; + + vm.startBroadcast(this.getEnv().ACCOUNT_PRIVATE_KEY); + + address starknetAddress; + if (this.isLocal()) { + starknetAddress = address(new StarknetMessagingLocal()); + } else { + starknetAddress = this.getEnv().STARKNET_ADDRESS; + } + vm.serializeString( + json, + "StarknetAddress", + vm.toString(starknetAddress) + ); + + address tokenAddress; + + if (this.isLocal() /*|| this.isGoerli()*/) { + tokenAddress = address(new Token()); + } else { + tokenAddress = this.getEnv().TOKEN_ADDRESS; + } + vm.serializeString(json, "Token", vm.toString(tokenAddress)); + + address l1DojoBridge = factory.deploy( + BRIDGE_SALT, + abi.encodePacked( + type(L1DojoBridge).creationCode, + abi.encode( + starknetAddress, + tokenAddress, + this.getEnv().L2_BRIDGE_ADDRESS + ) + ) + ); + vm.serializeString(json, "L1DojoBridge", vm.toString(l1DojoBridge)); + + vm.stopBroadcast(); + + string memory data = vm.serializeBool(json, "success", true); + + string memory localLogs = "./logs/"; + vm.createDir(localLogs, true); + vm.writeJson( + data, + string.concat(localLogs, this.getEnv().ENV, ".json") + ); + } +} + +contract GetBridgeAddress is BaseScript { + function setUp() public {} + + function run() public returns (address bridgeAddress) { + CREATE3Factory factory = CREATE3Factory( + this.getEnv().CREATE3_FACTORY_ADDRESS + ); + + vm.startBroadcast(this.getEnv().ACCOUNT_PRIVATE_KEY); + + bridgeAddress = factory.getDeployed( + this.getEnv().ACCOUNT_ADDRESS, + BRIDGE_SALT + ); + + vm.stopBroadcast(); + } +} + +contract Create3 is BaseScript { + function setUp() public {} + + function run() public returns (address) { + if (this.isLocal()) { + vm.startBroadcast(this.getEnv().ACCOUNT_PRIVATE_KEY); + CREATE3Factory factory = new CREATE3Factory(); + return address(factory); + } else { + console.log( + "Already deployed on", + this.getEnv().ENV, + "at", + this.getEnv().CREATE3_FACTORY_ADDRESS + ); + return address(this.getEnv().CREATE3_FACTORY_ADDRESS); + } + } +} diff --git a/examples/bridge/l1/src/L1DojoBridge.sol b/examples/bridge/l1/src/L1DojoBridge.sol new file mode 100644 index 00000000..1067af92 --- /dev/null +++ b/examples/bridge/l1/src/L1DojoBridge.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +interface IERC20Like { + function transfer(address to, uint256 amount) external returns (bool); + + function transferFrom( + address from, + address to, + uint256 value + ) external returns (bool success); +} + +interface IStarknetCore { + /** + Sends a message to an L2 contract. + + Returns the hash of the message. + */ + function sendMessageToL2( + uint256 toAddress, + uint256 selector, + uint256[] calldata payload + ) external payable returns (bytes32); + + /** + Consumes a message that was sent from an L2 contract. + + Returns the hash of the message. + */ + function consumeMessageFromL2( + uint256 fromAddress, + uint256[] calldata payload + ) external returns (bytes32); +} + +contract L1DojoBridge { + /// @notice The Starknet Core contract address on L1 + address public immutable starknet; + + /// @notice The $TOKEN ERC20 contract address on L1 + address public immutable l1Token; + + /// @notice The L2 address of the $TOKEN bridge, the counterpart to this contract + uint256 public immutable l2Bridge; + + event LogDeposit( + address indexed l1Sender, + uint256 amount, + uint256 l2Recipient + ); + event LogWithdrawal(address indexed l1Recipient, uint256 amount, uint256 l2TxHash); + + // 2 ** 251 + 17 * 2 ** 192 + 1; + uint256 private constant CAIRO_PRIME = + 3618502788666131213697322783095070105623107215331596699973092056135872020481; + + // from starkware.starknet.compiler.compile import get_selector_from_name + // print(get_selector_from_name('handle_deposit')) + uint256 private constant DEPOSIT_SELECTOR = + 1285101517810983806491589552491143496277809242732141897358598292095611420389; + + // operation ID sent in the L2 -> L1 message + uint256 private constant PROCESS_WITHDRAWAL = 1; + + function splitUint256( + uint256 value + ) internal pure returns (uint256, uint256) { + uint256 low = value & ((1 << 128) - 1); + uint256 high = value >> 128; + return (low, high); + } + + constructor(address _starknet, address _l1Token, uint256 _l2Bridge) { + require(_l2Bridge < CAIRO_PRIME, "Invalid L2 bridge address"); + + starknet = _starknet; + l1Token = _l1Token; + l2Bridge = _l2Bridge; + } + + /// @notice Function used to bridge $TOKEN from L1 to L2 + /// @param amount How many $TOKEN to send from msg.sender + /// @param l2Recipient To which L2 address should we deposit the $TOKEN to + /// @param fee Compulsory fee paid to the sequencer for passing on the message + function deposit( + uint256 amount, + uint256 l2Recipient, + uint256 fee + ) external payable { + require(amount > 0, "Amount is 0"); + require( + l2Recipient != 0 && + l2Recipient != l2Bridge && + l2Recipient < CAIRO_PRIME, + "Invalid L2 recipient" + ); + + uint256[] memory payload = new uint256[](3); + payload[0] = l2Recipient; + (payload[1], payload[2]) = splitUint256(amount); + + IERC20Like(l1Token).transferFrom(msg.sender, address(this), amount); + IStarknetCore(starknet).sendMessageToL2{value: fee}( + l2Bridge, + DEPOSIT_SELECTOR, + payload + ); + + emit LogDeposit(msg.sender, amount, l2Recipient); + } + + /// @notice Function to process the L2 withdrawal + /// @param amount How many $TOKEN were sent from L2 + /// @param l1Recipient Recipient of the (de)bridged $TOKEN + /// @param l2TxHash l2 tx_hash for matching txs in ui + function withdraw(uint256 amount, address l1Recipient, uint256 l2TxHash) external { + uint256[] memory payload = new uint256[](4); + payload[0] = PROCESS_WITHDRAWAL; + payload[1] = uint256(uint160(l1Recipient)); + (payload[2], payload[3]) = splitUint256(amount); + + // The call to consumeMessageFromL2 will succeed only if a + // matching L2->L1 message exists and is ready for consumption. + IStarknetCore(starknet).consumeMessageFromL2(l2Bridge, payload); + IERC20Like(l1Token).transfer(l1Recipient, amount); + + emit LogWithdrawal(l1Recipient, amount, l2TxHash); + } +} diff --git a/examples/bridge/l1/src/StarknetMessagingLocal.sol b/examples/bridge/l1/src/StarknetMessagingLocal.sol new file mode 100644 index 00000000..100d71f2 --- /dev/null +++ b/examples/bridge/l1/src/StarknetMessagingLocal.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.0; + +import "starknet/StarknetMessaging.sol"; + +/** + @notice Interface related to local messaging for Starknet. +*/ +interface IStarknetMessagingLocal { + function addMessageHashesFromL2( + uint256[] calldata msgHashes + ) + external + payable; +} + +/** + @title A superset of StarknetMessaging to support + local development by adding a way to directly register + a message hash ready to be consumed, without waiting the block + to be verified. + + @dev The idea is that, to not wait on the block to be proven, + this messaging contract can receive directly a hash of a message + to be considered as `received`. This message can then be consumed normally. + + DISCLAIMER: + The purpose of this contract is for local development only. +*/ +contract StarknetMessagingLocal is StarknetMessaging, IStarknetMessagingLocal { + + /** + @notice Hashes were added. + */ + event MessageHashesAddedFromL2( + uint256[] hashes + ); + + /** + @notice Adds the hashes of messages from L2. + + @param msgHashes Hashes to register as consumable. + */ + function addMessageHashesFromL2( + uint256[] calldata msgHashes + ) + external + payable + { + for (uint256 i = 0; i < msgHashes.length; i++) { + bytes32 hash = bytes32(msgHashes[i]); + l2ToL1Messages()[hash] += 1; + } + + emit MessageHashesAddedFromL2(msgHashes); + } + +} \ No newline at end of file diff --git a/examples/bridge/l1/src/Token.sol b/examples/bridge/l1/src/Token.sol new file mode 100644 index 00000000..4e34b69c --- /dev/null +++ b/examples/bridge/l1/src/Token.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract Token is ERC20, Ownable { + constructor() ERC20("Token", "TOKEN") {} + + function mint(address to, uint256 amount) external onlyOwner { + _mint(to, amount); + } + + function faucet() external { + _mint(msg.sender, 1_000 ether); + } +} diff --git a/examples/bridge/l1/test/Token.sol b/examples/bridge/l1/test/Token.sol new file mode 100644 index 00000000..43048031 --- /dev/null +++ b/examples/bridge/l1/test/Token.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console2} from "forge-std/Test.sol"; +import {Token} from "../src/Token.sol"; + +contract TokenTest is Test { + Token public token; + + function setUp() public { + token = new Token(); + } + +} diff --git a/examples/bridge/sn/.env.local b/examples/bridge/sn/.env.local new file mode 100644 index 00000000..84c44c28 --- /dev/null +++ b/examples/bridge/sn/.env.local @@ -0,0 +1,15 @@ +ENV=local + +RPC_URL=http://localhost:5050 + + +ACCOUNT_ADDRESS=0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973 +ACCOUNT_PRIVATE_KEY=0x1800000000300000180000000000030000000000003006001800006600 + +L1_BRIDGE_ADDRESS=0x7966c7B8b78b563a2B01CC2172F64633fe5214dA + +DOJO_TOKEN_NAME=TOKEN +DOJO_TOKEN_SYMBOL=TOK + +WORLD_NAME=dojobridge + diff --git a/examples/bridge/sn/.gitignore b/examples/bridge/sn/.gitignore new file mode 100644 index 00000000..1a8f8c10 --- /dev/null +++ b/examples/bridge/sn/.gitignore @@ -0,0 +1,3 @@ +.env +target + diff --git a/examples/bridge/sn/Makefile b/examples/bridge/sn/Makefile new file mode 100644 index 00000000..efdc5e80 --- /dev/null +++ b/examples/bridge/sn/Makefile @@ -0,0 +1,51 @@ +include .env.local + +export + +ifeq ($(origin KEYSTORE), undefined) + ACCOUNTS_PARAMS := --private-key ${ACCOUNT_PRIVATE_KEY} +else + ACCOUNTS_PARAMS := --keystore ${KEYSTORE} --password ${KEYSTORE_PASS} +endif + +PARAMS := --rpc-url ${RPC_URL} --account-address ${ACCOUNT_ADDRESS} ${ACCOUNTS_PARAMS} + +export SOZO_PARAMS = $(PARAMS) + +all: + @echo "********************************************************************" + @echo "ENV : ${ENV}" + @echo "WORLD_NAME : ${WORLD_NAME}" + @echo "********************************************************************" + @echo "RPC_URL : ${RPC_URL}" + @echo "ACCOUNT_ADDRESS : ${ACCOUNT_ADDRESS}" + @echo "KEYSTORE : ${KEYSTORE}" + @echo "ACCOUNT : ${ACCOUNT}" + @echo "********************************************************************" + @echo "L1_BRIDGE_ADDRESS : ${L1_BRIDGE_ADDRESS}" + @echo "DOJO_TOKEN_NAME : ${DOJO_TOKEN_NAME}" + @echo "DOJO_TOKEN_SYMBOL : ${DOJO_TOKEN_SYMBOL}" + @echo "********************************************************************" + + + +katana_msg: + katana --messaging anvil.messaging.json --dev + +katana_msg_slow: + katana --messaging anvil.messaging.json --dev --block-time 5000 + +build: + sozo build + +migrate: build + sozo migrate ${PARAMS} --name ${WORLD_NAME} --wait + +initialize: + ./scripts/initialize.sh + +migrate_and_init: migrate initialize + + + + diff --git a/examples/bridge/sn/Scarb.lock b/examples/bridge/sn/Scarb.lock new file mode 100644 index 00000000..7108cde3 --- /dev/null +++ b/examples/bridge/sn/Scarb.lock @@ -0,0 +1,30 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "bridge" +version = "0.1.0" +dependencies = [ + "dojo", + "token", +] + +[[package]] +name = "dojo" +version = "0.4.4" +source = "git+https://github.com/dojoengine/dojo?tag=v0.4.4#8afd8c903d71083b8487a5b4a0cfc9360dee0772" +dependencies = [ + "dojo_plugin", +] + +[[package]] +name = "dojo_plugin" +version = "0.3.11" +source = "git+https://github.com/dojoengine/dojo?tag=v0.3.11#1e651b5d4d3b79b14a7d8aa29a92062fcb9e6659" + +[[package]] +name = "token" +version = "0.0.0" +dependencies = [ + "dojo", +] diff --git a/examples/bridge/sn/Scarb.toml b/examples/bridge/sn/Scarb.toml new file mode 100644 index 00000000..59777f07 --- /dev/null +++ b/examples/bridge/sn/Scarb.toml @@ -0,0 +1,40 @@ +[package] +name = "bridge" +version = "0.1.0" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html + +[dependencies] +# not using workspace, otherwise all contracts/models get built & included in manifest +dojo = { git = "https://github.com/dojoengine/dojo", tag = "v0.4.4" } +token = { path = "../../../token" } + +[scripts] +katana_msg = "katana --messaging anvil.messaging.json --dev" +migrate = "sozo build && sozo migrate && ./scripts/initialize.sh local" + +get_balance = "sozo model get ERC20BalanceModel 0x792b78f74ae3631aa8795da847d57d7a45511f7c4711c4e81f2df2dac0fc177 0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973 --world 0x4f5310a18e23b0d3b20e58156d2a9552c16e6995b30d633911ee85ba5aeeeda --rpc-url http://localhost:5050" +withdraw = "sozo execute -c 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266,0x3635c9adc5dea00000,0 0x2ff2f9994ba7e039f50190cb3b3dc538d9abf7201acbe5a6a7aff686dd40d89 initiate_withdrawal" + +[tool.dojo.env] +# Katana +account_address = "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973" +private_key = "0x1800000000300000180000000000030000000000003006001800006600" +rpc_url = "http://localhost:5050" + +[tool.dojo.world] +name = "Dojo Bridge" +description = "Dojo Bridge" + +[[target.dojo]] +build-external-contracts = [ + "token::components::security::initializable::initializable_model", + "token::components::token::erc20::erc20_metadata::erc_20_metadata_model", + "token::components::token::erc20::erc20_balance::erc_20_balance_model", + "token::components::token::erc20::erc20_allowance::erc_20_allowance_model", + "token::components::token::erc20::erc20_mintable::erc_20_mintable_model", + "token::components::token::erc20::erc20_burnable::erc_20_burnable_model", + "token::components::token::erc20::erc20_bridgeable::erc_20_bridgeable_model", +] + + diff --git a/examples/bridge/sn/accounts/starkli_katana.json b/examples/bridge/sn/accounts/starkli_katana.json new file mode 100644 index 00000000..ab25a943 --- /dev/null +++ b/examples/bridge/sn/accounts/starkli_katana.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "variant": { + "type": "open_zeppelin", + "version": 1, + "public_key": "0x2b191c2f3ecf685a91af7cf72a43e7b90e2e41220175de5c4f7498981b10053" + }, + "deployment": { + "status": "deployed", + "class_hash": "0x04d07e40e93398ed3c76981e72dd1fd22557a78ce36c0515f679e27f0bb5bc5f", + "address": "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973" + } +} diff --git a/examples/bridge/sn/anvil.messaging.json b/examples/bridge/sn/anvil.messaging.json new file mode 100644 index 00000000..cd6154d3 --- /dev/null +++ b/examples/bridge/sn/anvil.messaging.json @@ -0,0 +1,10 @@ +{ + "chain": "ethereum", + "rpc_url": "http://127.0.0.1:8545", + "contract_address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "sender_address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "private_key": "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + "interval": 2, + "from_block": 0 +} + diff --git a/examples/bridge/sn/scripts/fund.sh b/examples/bridge/sn/scripts/fund.sh new file mode 100755 index 00000000..f37c3c5f --- /dev/null +++ b/examples/bridge/sn/scripts/fund.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -euo pipefail +pushd $(dirname "$0")/.. + +sleep 1 + +export ADDR_TO_FUND=$1 +export RPC_URL="http://localhost:5050" +export FEE_TOKEN_ADDRESS="0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7" + +starkli invoke --rpc $RPC_URL --account ./accounts/starkli_katana.json \ +--private-key 0x1800000000300000180000000000030000000000003006001800006600 \ +$FEE_TOKEN_ADDRESS transfer $ADDR_TO_FUND 1000000000000000000 0 diff --git a/examples/bridge/sn/scripts/initialize.sh b/examples/bridge/sn/scripts/initialize.sh new file mode 100755 index 00000000..9d1fb0c2 --- /dev/null +++ b/examples/bridge/sn/scripts/initialize.sh @@ -0,0 +1,67 @@ +#!/bin/bash +set -euo pipefail +pushd $(dirname "$0")/.. + +sleep 1 + +# export TX_SLEEP=200 # goerli ... +export TX_SLEEP=1 +export WORLD_ADDRESS=$(cat ./target/dev/manifest.json | jq -r '.world.address') + +export DOJO_TOKEN_ADDRESS=$(cat ./target/dev/manifest.json | jq -r '.contracts[] | select(.name == "dojo_bridge::dojo_token::dojo_token" ).address') +export DOJO_BRIDGE_ADDRESS=$(cat ./target/dev/manifest.json | jq -r '.contracts[] | select(.name == "dojo_bridge::dojo_bridge::dojo_bridge" ).address') + +export CAIRO_DOJO_TOKEN_NAME=$(starkli to-cairo-string $DOJO_TOKEN_NAME) +export CAIRO_DOJO_TOKEN_SYMBOL=$(starkli to-cairo-string $DOJO_TOKEN_SYMBOL) + +echo "---------------------------------------------------------------------------" +echo rpc : $RPC_URL +echo world : $WORLD_ADDRESS +echo "---------------------------------------------------------------------------" +echo token : $DOJO_TOKEN_ADDRESS +echo bridge: $DOJO_BRIDGE_ADDRESS +echo "---------------------------------------------------------------------------" +echo L1_BRIDGE_ADDRESS : $L1_BRIDGE_ADDRESS +echo DOJO_TOKEN_NAME : $DOJO_TOKEN_NAME $CAIRO_DOJO_TOKEN_NAME +echo DOJO_TOKEN_SYMBOL : $DOJO_TOKEN_SYMBOL $CAIRO_DOJO_TOKEN_SYMBOL +echo "---------------------------------------------------------------------------" +echo ACCOUNT_ADDRESS : $ACCOUNT_ADDRESS +echo SOZO_PARAMS : $SOZO_PARAMS +echo "---------------------------------------------------------------------------" + + +sleep 5 + +# enable system -> component authorizations +DOJO_TOKEN_COMPONENTS=( "ERC20MetadataModel" "ERC20BalanceModel" "ERC20AllowanceModel" "ERC20BridgeableModel" ) +DOJO_BRIDGE_COMPONENTS=("DojoBridgeModel") + + +for component in ${DOJO_TOKEN_COMPONENTS[@]}; do + sozo auth writer $component $DOJO_TOKEN_ADDRESS --world $WORLD_ADDRESS $SOZO_PARAMS + sleep $TX_SLEEP +done + +for component in ${DOJO_BRIDGE_COMPONENTS[@]}; do + sozo auth writer $component $DOJO_BRIDGE_ADDRESS --world $WORLD_ADDRESS $SOZO_PARAMS + sleep $TX_SLEEP +done + +printf "Default authorizations have been successfully set.\n" +sleep 2 +echo "Initialization..." + + +# dojo_token +# fn initializer(ref self: ContractState, name: felt252, symbol: felt252, l2_bridge_address: ContractAddress) +echo "Initialize token" +sozo execute $DOJO_TOKEN_ADDRESS initializer -c $CAIRO_DOJO_TOKEN_NAME,$CAIRO_DOJO_TOKEN_SYMBOL,$DOJO_BRIDGE_ADDRESS $SOZO_PARAMS +sleep $TX_SLEEP + +# dojo_bridge +# fn initializer(ref self: ContractState, l1_bridge: felt252, l2_token: ContractAddress) +echo "Initialize bridge" +sozo execute $DOJO_BRIDGE_ADDRESS initializer -c $L1_BRIDGE_ADDRESS,$DOJO_TOKEN_ADDRESS $SOZO_PARAMS +sleep $TX_SLEEP + + diff --git a/examples/bridge/sn/src/dojo_bridge.cairo b/examples/bridge/sn/src/dojo_bridge.cairo new file mode 100644 index 00000000..eb6653f6 --- /dev/null +++ b/examples/bridge/sn/src/dojo_bridge.cairo @@ -0,0 +1,192 @@ +use starknet::{ContractAddress, ClassHash}; +use dojo::world::IWorldDispatcher; + +#[starknet::interface] +trait IDojoBridge { + // IWorldProvider + fn world(self: @TState, ) -> IWorldDispatcher; + + // IUpgradeable + fn upgrade(ref self: TState, new_class_hash: ClassHash); + + // WITHOUT INTERFACE !!! + fn initializer(ref self: TState, l1_bridge: felt252, l2_token: ContractAddress); + fn initiate_withdrawal(ref self: TState, l1_recipient: felt252, amount: u256); + fn get_l1_bridge(self: @TState, ) -> felt252; + fn get_token(self: @TState, ) -> ContractAddress; + fn dojo_resource(self: @TState, ) -> felt252; +} + +/// +/// Model +/// + +#[derive(Model, Copy, Drop, Serde)] +struct DojoBridgeModel { + #[key] + bridge: ContractAddress, + // address L1 bridge contract address, the L1 counterpart to this contract + l1_bridge: felt252, + // Dojo ERC20 token on Starknet + l2_token: ContractAddress +} + + +#[dojo::contract] +mod dojo_bridge { + use super::DojoBridgeModel; + use starknet::ContractAddress; + use starknet::{get_contract_address, get_caller_address}; + + use token::components::token::erc20::erc20_bridgeable::{ + IERC20Bridgeable, IERC20BridgeableDispatcher, IERC20BridgeableDispatcherTrait + }; + + use token::components::security::initializable::initializable_component; + + component!(path: initializable_component, storage: initializable, event: InitializableEvent); + + #[storage] + struct Storage { + #[substorage(v0)] + initializable: initializable_component::Storage, + } + + #[event] + #[derive(Copy, Drop, starknet::Event)] + enum Event { + InitializableEvent: initializable_component::Event, + DepositHandled: DepositHandled, + WithdrawalInitiated: WithdrawalInitiated, + } + + #[derive(Copy, Drop, starknet::Event)] + struct DepositHandled { + recipient: ContractAddress, + amount: u256 + } + + #[derive(Copy, Drop, starknet::Event)] + struct WithdrawalInitiated { + #[key] + sender: ContractAddress, + recipient: felt252, + amount: u256 + } + + mod Errors { + const CALLER_IS_NOT_OWNER: felt252 = 'Bridge: caller is not owner'; + const INVALID_L1_ORIGIN: felt252 = 'Bridge: invalid L1 origin'; + + const L1_ADDRESS_CANNOT_BE_ZERO: felt252 = 'Bridge: L1 address cannot be 0'; + const L1_ADDRESS_OUT_OF_BOUNDS: felt252 = 'Bridge: L1 addr out of bounds'; + const INVALID_RECIPIENT: felt252 = 'Bridge: invalid recipient'; + } + + + // operation ID sent in the message payload to L1 + const PROCESS_WITHDRAWAL: felt252 = 1; + + // Ethereum addresses are bound to 2**160 + const ETH_ADDRESS_BOUND: u256 = 0x10000000000000000000000000000000000000000_u256; + + impl InitializableImpl = initializable_component::InitializableImpl; + impl InitializableInternalImpl = initializable_component::InternalImpl; + + // + // Initializer + // + + #[external(v0)] + #[generate_trait] + impl ERC20InitializerImpl of ERC20InitializerTrait { + fn initializer(ref self: ContractState, l1_bridge: felt252, l2_token: ContractAddress) { + assert( + self.world().is_owner(get_caller_address(), get_contract_address().into()), + Errors::CALLER_IS_NOT_OWNER + ); + + // one time bridge initialization + set!( + self.world(), DojoBridgeModel { bridge: get_contract_address(), l1_bridge, l2_token } + ); + + // reverts if already initialized + self.initializable.initialize(); + } + } + + // + // L1 Handler + // + + #[l1_handler] + fn handle_deposit( + ref self: ContractState, from_address: felt252, recipient: ContractAddress, amount: u256 + ) { + let data = self.get_data(); + assert(from_address == data.l1_bridge, Errors::INVALID_L1_ORIGIN); + + // mint token + let token_dispatcher = IERC20BridgeableDispatcher { contract_address: data.l2_token }; + token_dispatcher.mint(recipient, amount); + + let event = DepositHandled { recipient, amount }; + self.emit_event(event); + } + + // + // Impls + // + + #[external(v0)] + #[generate_trait] + impl DojoBridgeImpl of DojoBridgeTrait { + fn initiate_withdrawal(ref self: ContractState, l1_recipient: felt252, amount: u256) { + let data = self.get_data(); + let caller = get_caller_address(); + + assert(l1_recipient.is_non_zero(), Errors::L1_ADDRESS_CANNOT_BE_ZERO); + assert(l1_recipient.into() < ETH_ADDRESS_BOUND, Errors::L1_ADDRESS_OUT_OF_BOUNDS); + assert(l1_recipient != data.l1_bridge, Errors::INVALID_RECIPIENT); + + // burn token + let token_dispatcher = IERC20BridgeableDispatcher { contract_address: data.l2_token }; + token_dispatcher.burn(caller, amount); + + let message: Array = array![ + PROCESS_WITHDRAWAL, l1_recipient, amount.low.into(), amount.high.into() + ]; + + // send msg to L1 + starknet::syscalls::send_message_to_l1_syscall(data.l1_bridge, message.span()); + + let event = WithdrawalInitiated { sender: caller, recipient: l1_recipient, amount }; + self.emit_event(event); + } + + fn get_l1_bridge(self: @ContractState) -> felt252 { + self.get_data().l1_bridge + } + + fn get_token(self: @ContractState) -> ContractAddress { + self.get_data().l2_token + } + } + + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn get_data(self: @ContractState) -> DojoBridgeModel { + get!(self.world(), get_contract_address(), (DojoBridgeModel)) + } + + fn emit_event, +Drop, +Clone>( + ref self: ContractState, event: S + ) { + self.emit(event.clone()); + emit!(self.world(), event); + } + } +} + diff --git a/examples/bridge/sn/src/dojo_token.cairo b/examples/bridge/sn/src/dojo_token.cairo new file mode 100644 index 00000000..f0ae878d --- /dev/null +++ b/examples/bridge/sn/src/dojo_token.cairo @@ -0,0 +1,200 @@ +use starknet::{ContractAddress, ClassHash}; +use dojo::world::IWorldDispatcher; + +#[starknet::interface] +trait IDojoToken { + // IWorldProvider + fn world(self: @TState,) -> IWorldDispatcher; + + // IUpgradeable + fn upgrade(ref self: TState, new_class_hash: ClassHash); + + // IERC20MetadataTotalSupply + fn total_supply(self: @TState,) -> u256; + + // IERC20MetadataTotalSupplyCamel + fn totalSupply(self: @TState,) -> u256; + + // IERC20Balance + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + + // IERC20BalanceCamel + fn balanceOf(self: @TState, account: ContractAddress) -> u256; + fn transferFrom( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + + // IERC20Allowance + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; + + // IERC20Metadata + fn decimals(self: @TState,) -> u8; + fn name(self: @TState,) -> felt252; + fn symbol(self: @TState,) -> felt252; + + // IERC20SafeAllowance + fn decrease_allowance( + ref self: TState, spender: ContractAddress, subtracted_value: u256 + ) -> bool; + fn increase_allowance(ref self: TState, spender: ContractAddress, added_value: u256) -> bool; + + // IERC20SafeAllowanceCamel + fn decreaseAllowance(ref self: TState, spender: ContractAddress, subtractedValue: u256) -> bool; + fn increaseAllowance(ref self: TState, spender: ContractAddress, addedValue: u256) -> bool; + + // IERC20Bridgeable + fn burn(ref self: TState, account: ContractAddress, amount: u256); + fn l2_bridge_address(self: @TState,) -> ContractAddress; + fn mint(ref self: TState, recipient: ContractAddress, amount: u256); + + // WITHOUT INTERFACE !!! + fn initializer( + ref self: TState, name: felt252, symbol: felt252, l2_bridge_address: ContractAddress + ); + fn dojo_resource(self: @TState,) -> felt252; +} + + +#[dojo::contract] +mod dojo_token { + use token::erc20::interface; + use starknet::ContractAddress; + use starknet::{get_caller_address, get_contract_address}; + + use token::components::security::initializable::initializable_component; + + use token::components::token::erc20::erc20_metadata::erc20_metadata_component; + use token::components::token::erc20::erc20_balance::erc20_balance_component; + use token::components::token::erc20::erc20_allowance::erc20_allowance_component; + use token::components::token::erc20::erc20_mintable::erc20_mintable_component; + use token::components::token::erc20::erc20_burnable::erc20_burnable_component; + use token::components::token::erc20::erc20_bridgeable::erc20_bridgeable_component; + + component!(path: initializable_component, storage: initializable, event: InitializableEvent); + + component!(path: erc20_metadata_component, storage: erc20_metadata, event: ERC20MetadataEvent); + component!(path: erc20_balance_component, storage: erc20_balance, event: ERC20BalanceEvent); + component!( + path: erc20_allowance_component, storage: erc20_allowance, event: ERC20AllowanceEvent + ); + component!(path: erc20_mintable_component, storage: erc20_mintable, event: ERC20MintableEvent); + component!(path: erc20_burnable_component, storage: erc20_burnable, event: ERC20BurnableEvent); + component!( + path: erc20_bridgeable_component, storage: erc20_bridgeable, event: ERC20BridgeableEvent + ); + + #[storage] + struct Storage { + #[substorage(v0)] + initializable: initializable_component::Storage, + #[substorage(v0)] + erc20_metadata: erc20_metadata_component::Storage, + #[substorage(v0)] + erc20_balance: erc20_balance_component::Storage, + #[substorage(v0)] + erc20_allowance: erc20_allowance_component::Storage, + #[substorage(v0)] + erc20_mintable: erc20_mintable_component::Storage, + #[substorage(v0)] + erc20_burnable: erc20_burnable_component::Storage, + #[substorage(v0)] + erc20_bridgeable: erc20_bridgeable_component::Storage, + } + + #[event] + #[derive(Copy, Drop, starknet::Event)] + enum Event { + InitializableEvent: initializable_component::Event, + ERC20MetadataEvent: erc20_metadata_component::Event, + ERC20BalanceEvent: erc20_balance_component::Event, + ERC20AllowanceEvent: erc20_allowance_component::Event, + ERC20MintableEvent: erc20_mintable_component::Event, + ERC20BurnableEvent: erc20_burnable_component::Event, + ERC20BridgeableEvent: erc20_bridgeable_component::Event, + } + + mod Errors { + const CALLER_IS_NOT_OWNER: felt252 = 'ERC20: caller is not owner'; + } + + + impl InitializableImpl = initializable_component::InitializableImpl; + + #[abi(embed_v0)] + impl ERC20MetadataTotalSupplyImpl = + erc20_metadata_component::ERC20MetadataTotalSupplyImpl; + + #[abi(embed_v0)] + impl ERC20MetadataTotalSupplyCamelImpl = + erc20_metadata_component::ERC20MetadataTotalSupplyCamelImpl; + + #[abi(embed_v0)] + impl ERC20BalanceImpl = + erc20_balance_component::ERC20BalanceImpl; + + #[abi(embed_v0)] + impl ERC20BalanceCamelImpl = + erc20_balance_component::ERC20BalanceCamelImpl; + + #[abi(embed_v0)] + impl ERC20AllowanceImpl = + erc20_allowance_component::ERC20AllowanceImpl; + + #[abi(embed_v0)] + impl ERC20MetadataImpl = + erc20_metadata_component::ERC20MetadataImpl; + + #[abi(embed_v0)] + impl ERC20SafeAllowanceImpl = + erc20_allowance_component::ERC20SafeAllowanceImpl; + + #[abi(embed_v0)] + impl ERC20SafeAllowanceCamelImpl = + erc20_allowance_component::ERC20SafeAllowanceCamelImpl; + + #[abi(embed_v0)] + impl ERC20BridgeableImpl = + erc20_bridgeable_component::ERC20BridgeableImpl; + + // + // Internal Impls + // + + impl InitializableInternalImpl = initializable_component::InternalImpl; + impl ERC20MetadataInternalImpl = erc20_metadata_component::InternalImpl; + impl ERC20BalanceInternalImpl = erc20_balance_component::InternalImpl; + impl ERC20AllowanceInternalImpl = erc20_allowance_component::InternalImpl; + impl ERC20MintableInternalImpl = erc20_mintable_component::InternalImpl; + impl ERC20BurnableInternalImpl = erc20_burnable_component::InternalImpl; + impl ERC20BridgeableInternalImpl = erc20_bridgeable_component::InternalImpl; + + // + // Initializer + // + + #[external(v0)] + #[generate_trait] + impl ERC20InitializerImpl of ERC20InitializerTrait { + fn initializer( + ref self: ContractState, + name: felt252, + symbol: felt252, + l2_bridge_address: ContractAddress, + ) { + assert( + self.world().is_owner(get_caller_address(), get_contract_address().into()), + Errors::CALLER_IS_NOT_OWNER + ); + + self.erc20_metadata.initialize(name, symbol, 18); + self.erc20_bridgeable.initialize(l2_bridge_address); + + self.initializable.initialize(); + } + } +} diff --git a/examples/bridge/sn/src/lib.cairo b/examples/bridge/sn/src/lib.cairo new file mode 100644 index 00000000..2c3244b5 --- /dev/null +++ b/examples/bridge/sn/src/lib.cairo @@ -0,0 +1,9 @@ +mod dojo_token; +mod dojo_bridge; + +#[cfg(test)] +mod tests { + mod tests; +} + + diff --git a/examples/bridge/sn/src/tests/tests.cairo b/examples/bridge/sn/src/tests/tests.cairo new file mode 100644 index 00000000..d6dbb855 --- /dev/null +++ b/examples/bridge/sn/src/tests/tests.cairo @@ -0,0 +1,132 @@ +use core::array::SpanTrait; +use starknet::ContractAddress; +use starknet::testing; +use zeroable::Zeroable; + +use integer::BoundedInt; +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; +use dojo::test_utils::spawn_test_world; +use token::tests::constants::{ + ZERO, OWNER, SPENDER, RECIPIENT, BRIDGE, NAME, SYMBOL, DECIMALS, SUPPLY, VALUE +}; + +use token::tests::utils; + +use token::components::token::erc20::erc20_metadata::{erc_20_metadata_model, ERC20MetadataModel,}; +use token::components::token::erc20::erc20_metadata::erc20_metadata_component::{ + ERC20MetadataImpl, ERC20MetadataTotalSupplyImpl, InternalImpl as ERC20MetadataInternalImpl +}; + +use token::components::token::erc20::erc20_balance::{erc_20_balance_model, ERC20BalanceModel,}; +use token::components::token::erc20::erc20_balance::erc20_balance_component::{ + Transfer, ERC20BalanceImpl, InternalImpl as ERC20BalanceInternalImpl +}; + +use token::components::token::erc20::erc20_allowance::{ + erc_20_allowance_model, ERC20AllowanceModel, +}; +use token::components::token::erc20::erc20_allowance::erc20_allowance_component::{ + Approval, ERC20AllowanceImpl, InternalImpl as ERC20AllownceInternalImpl, ERC20SafeAllowanceImpl, + ERC20SafeAllowanceCamelImpl +}; + +use token::components::token::erc20::erc20_bridgeable::{ + erc_20_bridgeable_model, ERC20BridgeableModel +}; +use token::components::token::erc20::erc20_bridgeable::erc20_bridgeable_component::{ + ERC20BridgeableImpl +}; + +use token::components::token::erc20::erc20_mintable::erc20_mintable_component::InternalImpl as ERC20MintableInternalImpl; +use token::components::token::erc20::erc20_burnable::erc20_burnable_component::InternalImpl as ERC20BurnableInternalImpl; + +use bridge::dojo_token::{ + dojo_token, IDojoToken, IDojoTokenDispatcher, IDojoTokenDispatcherTrait +}; +use bridge::dojo_bridge::{ + dojo_bridge_model, dojo_bridge, IDojoBridge, IDojoBridgeDispatcher, IDojoBridgeDispatcherTrait +}; + +use token::components::tests::token::erc20::test_erc20_allowance::{ + assert_event_approval, assert_only_event_approval +}; +use token::components::tests::token::erc20::test_erc20_balance::{ + assert_event_transfer, assert_only_event_transfer +}; + + +const L1BRIDGE: felt252 = 'L1BRIDGE'; + +// +// Setup +// + +fn setup() -> (IWorldDispatcher, IDojoTokenDispatcher, IDojoBridgeDispatcher) { + let world = spawn_test_world( + array![ + erc_20_allowance_model::TEST_CLASS_HASH, + erc_20_balance_model::TEST_CLASS_HASH, + erc_20_metadata_model::TEST_CLASS_HASH, + erc_20_bridgeable_model::TEST_CLASS_HASH, + dojo_bridge_model::TEST_CLASS_HASH, + ] + ); + + // deploy token + let mut dojo_token_dispatcher = IDojoTokenDispatcher { + contract_address: world + .deploy_contract('salt', dojo_token::TEST_CLASS_HASH.try_into().unwrap()) + }; + + // deploy bridge + let mut dojo_bridge_dispatcher = IDojoBridgeDispatcher { + contract_address: world + .deploy_contract('salt', dojo_bridge::TEST_CLASS_HASH.try_into().unwrap()) + }; + + // setup auth for dojo_token + world.grant_writer('ERC20AllowanceModel', dojo_token_dispatcher.contract_address); + world.grant_writer('ERC20BalanceModel', dojo_token_dispatcher.contract_address); + world.grant_writer('ERC20MetadataModel', dojo_token_dispatcher.contract_address); + world.grant_writer('ERC20BridgeableModel', dojo_token_dispatcher.contract_address); + + // setup auth for dojo_bridge + world.grant_writer('DojoBridgeModel', dojo_bridge_dispatcher.contract_address); + + + // initialize dojo_token + dojo_token_dispatcher.initializer(NAME, SYMBOL, dojo_bridge_dispatcher.contract_address); + + // initialize dojo_bridge + dojo_bridge_dispatcher.initializer( L1BRIDGE, dojo_token_dispatcher.contract_address); + dojo_bridge_dispatcher.initializer( L1BRIDGE, dojo_token_dispatcher.contract_address); + + // drop all events + utils::drop_all_events(dojo_token_dispatcher.contract_address); + utils::drop_all_events(dojo_bridge_dispatcher.contract_address); + utils::drop_all_events(world.contract_address); + + (world, dojo_token_dispatcher, dojo_bridge_dispatcher) +} + + +// +// initializer +// + +#[test] +#[available_gas(30000000)] +fn test_initializers() { + let (world, mut dojo_token, mut dojo_bridge) = setup(); + + assert(dojo_token.total_supply() == 0, 'Should eq 0'); + assert(dojo_token.name() == NAME, 'Name should be NAME'); + assert(dojo_token.symbol() == SYMBOL, 'Symbol should be SYMBOL'); + assert(dojo_token.decimals() == DECIMALS, 'Decimals should be 18'); + + assert(dojo_token.l2_bridge_address() == dojo_bridge.contract_address, 'invalid l2_bridge_address'); + + assert(dojo_bridge.get_l1_bridge() == L1BRIDGE, 'Should be L1BRIDGE'); + assert(dojo_bridge.get_token() == dojo_token.contract_address, 'Invalid get_token'); + +}