diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 00000000..e4097b7e --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,44 @@ +{ + "projectName": "starklane", + "projectOwner": "ScreenshotLabs", + "repoType": "github", + "repoHost": "https://github.com", + "files": [ + "README.md" + ], + "imageSize": 100, + "commit": true, + "commitConvention": "angular", + "contributorsPerLine": 7, + "linkToUsage": true, + "contributors": [ + { + "login": "remiroyc", + "name": "Rémi", + "avatar_url": "https://avatars.githubusercontent.com/u/11146088?v=4", + "profile": "https://github.com/remiroyc", + "contributions": [ + "code" + ] + }, + { + "login": "kwiss", + "name": "Christophe", + "avatar_url": "https://avatars.githubusercontent.com/u/243668?v=4", + "profile": "https://github.com/kwiss", + "contributions": [ + "code", + "design" + ] + }, + { + "login": "gershon", + "name": "Paul", + "avatar_url": "https://avatars.githubusercontent.com/u/55589?v=4", + "profile": "https://github.com/gershon", + "contributions": [ + "code" + ] + } + ] +} diff --git a/.env.sample b/.env.sample new file mode 100644 index 00000000..31e663dc --- /dev/null +++ b/.env.sample @@ -0,0 +1,6 @@ + PRIVATE_KEY= + ALCHEMY_KEY= + HOSTNAME_L1= + HOSTNAME_L2= + ETHERSCAN_API_KEY= + L2_NETWORK= \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..5b999efa --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,10 @@ +module.exports = { + root: true, + // This tells ESLint to load the config from the package `eslint-config-custom` + extends: ["custom"], + settings: { + next: { + rootDir: ["apps/*/"], + }, + }, +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..067b6fe9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +/**/node_modules +.pnp +.pnp.js + +# testing +coverage + +# next.js +.next/ +out/ +build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# turbo +.turbo + +.env +coverage.json +typechain +typechain-types +cache +artifacts +*.key + +starknet-artifacts \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..5a1ba22c --- /dev/null +++ b/.prettierignore @@ -0,0 +1,33 @@ +node_modules + +#Hardhat files +cache +artifacts + +#Python artifacts +.venv* +__pycache__ +.pytest_cache + +#Starknet plugin +starknet-artifacts + +#NPM lock file +package-lock.json + +#Environment variables file +.env + +#deployment artifacts folder +deployment + +# Yarn error file +yarn-error.log + +# Apple related file +.DS_Store + +#IDEs +.idea + +starknet-artifacts/**/*.json \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +{} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..1f49873a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,5 @@ +# Contributing + +For issues to work on, [see the issues tab](https://github.com/ScreenshotLabs/starklane/issues) + +To help you get your feet wet and get you familiar with our contribution process, we have a list of **good first issues** that contain bugs that have a relatively limited scope. This is a great place to get started. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..30921aa5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM python:3.11 + +ENV HOSTNAME_L1 testnet-l1 +ENV HOSTNAME_L2 testnet-l2 + +RUN apt update -y && apt upgrade -y && apt install curl git libssl-dev libgmp3-dev -y + +# Copy folder +COPY . starklane +WORKDIR starklane + +# Install Python dependencies +RUN rm -rf .venv && python -m venv .venv +RUN . .venv/bin/activate +RUN python -m pip install --upgrade pip && pip install poetry && poetry install + +# Install Node 16 +RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - && \ + apt-get install -y nodejs + +# Install Yarn +RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ + echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ + apt update -y && \ + apt install yarn -y + +# Install Node dependencies +RUN yarn + +# Build Cairo files +RUN yarn build:l2 \ No newline at end of file diff --git a/README.md b/README.md index cfe07b55..1754e1dc 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,70 @@ -# starklane +# Starklane + The Starklane NFT Bridge: seamless transfer of NFTs between ETH L1 & Starknet L2. Smart contracts, user-friendly interface, secure & efficient solution. Experience the future of NFT ownership today + +[![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](https://lbesson.mit-license.org/) + + + +[![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-) + + + +## + +## ➡️ L1 - L2 Flow + +- The L1 gateway sends a message to the L2 gateway + +- The L2 bridge contract verifies the presence of the L1 address in the registry structure `_l1_to_l2_addresses` + +- If the L2 contract doesn't exist, the Universal Deployer Contract is automatically called to deploy a default ERC-721 contract, resulting in a replica of the L1 contract on L2. + +- The bridge contract has the authority to mint a new token on the deployed smart contract. + +## ⬅️ L2 - L1 Flow + +TBD + +## Quickstart + +### Install dependencies + +`yarn` + +### Build all packages + +`yarn build` + +## Disclaimer + +These contracts are only given as an example. They HAVE NOT undergone any audit. They SHOULD NOT be used for any production level application. + +## Contributors ✨ + + + + + + + + + + + + + + + + + +
Rémi
Rémi

💻
Christophe
Christophe

💻 🎨
Paul
Paul

💻
+ + Add your contributions + +
+ + + + + diff --git a/apps/blockchain/contracts/ethereum/Bridge.sol b/apps/blockchain/contracts/ethereum/Bridge.sol new file mode 100644 index 00000000..b1e77d2a --- /dev/null +++ b/apps/blockchain/contracts/ethereum/Bridge.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import "./interfaces/IStarknetMessaging.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +interface NFTContract is IERC721 { + function name() external view returns (string memory); + + function symbol() external view returns (string memory); + + function tokenURI(uint256 tokenId) external view returns (string memory); +} + +contract Bridge is Ownable { + IStarknetMessaging public starknetCore; + uint256 public selector; + uint256 public l2GatewayAddress; + + constructor(address starknetCore_) { + require( + starknetCore_ != address(0), + "Gateway/invalid-starknet-core-address" + ); + starknetCore = IStarknetMessaging(starknetCore_); + } + + function setSelector(uint256 value) external onlyOwner { + selector = value; + } + + function setL2GatewayAddress(uint256 value) external onlyOwner { + l2GatewayAddress = value; + } + + function strToUint(string memory text) public pure returns (uint256 res) { + bytes32 stringInBytes32 = bytes32(bytes(text)); + uint256 strLen = bytes(text).length; // TODO: cannot be above 32 + require(strLen <= 32, "String cannot be longer than 32"); + + uint256 shift = 256 - 8 * strLen; + + uint256 stringInUint256; + assembly { + stringInUint256 := shr(shift, stringInBytes32) + } + return stringInUint256; + } + + function deposit( + address l1TokenAddress, + uint256 l2OwnerAddress, + uint256 tokenId + ) public payable { + NFTContract tokenContract = NFTContract(l1TokenAddress); + + // optimistic transfer, should revert if no approved or not owner + tokenContract.transferFrom(msg.sender, address(this), tokenId); + + string memory symbol = tokenContract.symbol(); + string memory name = tokenContract.name(); + string memory tokenUri = tokenContract.tokenURI(tokenId); + + uint256[] memory payload = new uint256[](6); + + payload[0] = uint256(uint160(l1TokenAddress)); // l1_contract_address + payload[1] = l2OwnerAddress; // to + payload[2] = tokenId; + payload[3] = strToUint(name); + payload[4] = strToUint(symbol); + payload[5] = strToUint(tokenUri); + + starknetCore.sendMessageToL2{value: msg.value}( + l2GatewayAddress, + selector, + payload + ); + } + + // TO REMOVE + function forceWithdraw(address l1_contract_address, uint256 tokenId) + public + onlyOwner + { + IERC721 tokenContract = NFTContract(l1_contract_address); + tokenContract.transferFrom(address(this), msg.sender, tokenId); + } +} diff --git a/apps/blockchain/contracts/ethereum/interfaces/IStarknetMessaging.sol b/apps/blockchain/contracts/ethereum/interfaces/IStarknetMessaging.sol new file mode 100644 index 00000000..161e8dd2 --- /dev/null +++ b/apps/blockchain/contracts/ethereum/interfaces/IStarknetMessaging.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.9; + +import "./IStarknetMessagingEvents.sol"; + +interface IStarknetMessaging is IStarknetMessagingEvents { + /** + 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 messageCancellationDelay() seconds + after the call to startL1ToL2MessageCancellation(). + + 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/apps/blockchain/contracts/ethereum/interfaces/IStarknetMessagingEvents.sol b/apps/blockchain/contracts/ethereum/interfaces/IStarknetMessagingEvents.sol new file mode 100644 index 00000000..e7043c29 --- /dev/null +++ b/apps/blockchain/contracts/ethereum/interfaces/IStarknetMessagingEvents.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.9; + +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/apps/blockchain/contracts/starknet/bridge.cairo b/apps/blockchain/contracts/starknet/bridge.cairo new file mode 100644 index 00000000..6788a8d9 --- /dev/null +++ b/apps/blockchain/contracts/starknet/bridge.cairo @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: MIT + +%lang starknet + +from starkware.cairo.common.cairo_builtins import HashBuiltin +from starkware.cairo.common.uint256 import Uint256, uint256_unsigned_div_rem +from starkware.starknet.common.syscalls import get_caller_address +from starkware.cairo.common.math import assert_not_zero +from starkware.cairo.common.alloc import alloc +from openzeppelin.access.ownable.library import Ownable +from starkware.starknet.common.syscalls import get_contract_address + +@contract_interface +namespace IUniversalDeployerContract { + func deployContract( + classHash: felt, salt: felt, unique: felt, calldata_len: felt, calldata: felt* + ) -> (address: felt) { + } +} + +@contract_interface +namespace IDefaultToken { + func permissionedMint(to: felt, tokenId: Uint256, data_len: felt, data: felt*, tokenURI: felt) { + } +} + +@event +func collection_created(address: felt) { +} + +@storage_var +func _l1_to_l2_addresses(l1_address: felt) -> (l1_address: felt) { +} + +@storage_var +func _contract_salt() -> (salt: felt) { +} + +@storage_var +func _token_class_hash() -> (res: felt) { +} + +@storage_var +func _udc_contract() -> (res: felt) { +} + +@constructor +func constructor{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( + owner: felt, class_hash: felt, initial_salt: felt, udc_contract: felt +) { + Ownable.initializer(owner); + _token_class_hash.write(class_hash); + _contract_salt.write(initial_salt); + _udc_contract.write(udc_contract); + return (); +} + +@l1_handler +func deposit{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( + from_address: felt, + l1_contract_address: felt, + to: felt, + token_id: felt, + name: felt, + symbol: felt, + token_uri: felt, +) { + deposit_token(l1_contract_address, name, symbol, to, token_uri, token_id); + return (); +} + +func deposit_token{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( + l1_contract_address: felt, name: felt, symbol: felt, to: felt, token_uri: felt, token_id: felt +) { + alloc_locals; + + let uint_token_id = Uint256(token_id, 0); + let (contract_address) = _l1_to_l2_addresses.read(l1_contract_address); + if (contract_address == 0) { + let (deployed_contract_address) = deploy_new_contract(l1_contract_address, name, symbol); + mint_token(deployed_contract_address, to, token_uri, uint_token_id); + } else { + mint_token(contract_address, to, token_uri, uint_token_id); + } + return (); +} + +func mint_token{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( + deployed_contract_address: felt, to: felt, token_uri: felt, token_id: Uint256 +) -> (deployed_contract_address: felt) { + alloc_locals; + let (data: felt*) = alloc(); + + IDefaultToken.permissionedMint( + contract_address=deployed_contract_address, + to=to, + tokenId=token_id, + data_len=0, + data=data, + tokenURI=token_uri, + ); + + return (deployed_contract_address=deployed_contract_address); +} + +func deploy_new_contract{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( + l1_contract_address: felt, name: felt, symbol: felt +) -> (contract_address: felt) { + alloc_locals; + + let (bridge_contract_address) = get_contract_address(); + let (message_payload: felt*) = alloc(); + + assert message_payload[0] = bridge_contract_address; + assert message_payload[1] = name; + assert message_payload[2] = symbol; + + let (token_class_hash) = _token_class_hash.read(); + let (udc_contract_address) = _udc_contract.read(); + let (salt) = _contract_salt.read(); + let new_salt = salt + 1; + + let (deployed_contract_address) = IUniversalDeployerContract.deployContract( + contract_address=udc_contract_address, + classHash=token_class_hash, + salt=new_salt, + unique=0, + calldata_len=3, + calldata=message_payload, + ); + + _contract_salt.write(new_salt); + _l1_to_l2_addresses.write(l1_contract_address, deployed_contract_address); + + collection_created.emit(address=deployed_contract_address); + return (contract_address=deployed_contract_address); +} + +@external +func set_salt{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(salt: felt) { + Ownable.assert_only_owner(); + _contract_salt.write(salt); + return (); +} + +@external +func set_udc_contract{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( + udc_contract: felt +) { + Ownable.assert_only_owner(); + _udc_contract.write(udc_contract); + return (); +} + +@view +func get_token_class_hash{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> ( + res: felt +) { + let (res) = _token_class_hash.read(); + return (res=res); +} + +@view +func get_salt{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> (salt: felt) { + let (salt) = _contract_salt.read(); + return (salt=salt); +} + +@view +func get_l2_address{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( + l1_address: felt +) -> (l2_address: felt) { + let (l2_address) = _l1_to_l2_addresses.read(l1_address); + return (l2_address,); +} diff --git a/apps/blockchain/contracts/starknet/default_token.cairo b/apps/blockchain/contracts/starknet/default_token.cairo new file mode 100644 index 00000000..401f5694 --- /dev/null +++ b/apps/blockchain/contracts/starknet/default_token.cairo @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: MIT + +%lang starknet + +from starkware.cairo.common.cairo_builtins import HashBuiltin +from starkware.cairo.common.uint256 import Uint256 + +from openzeppelin.token.erc721.library import ERC721 +from openzeppelin.introspection.erc165.library import ERC165 +from openzeppelin.access.ownable.library import Ownable + +@constructor +func constructor{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( + owner: felt, name: felt, symbol: felt +) { + ERC721.initializer(name, symbol); + Ownable.initializer(owner); + return (); +} + +// +// Getters +// + +@view +func supportsInterface{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( + interfaceId: felt +) -> (success: felt) { + return ERC165.supports_interface(interfaceId); +} + +@view +func name{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> (name: felt) { + return ERC721.name(); +} + +@view +func symbol{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> (symbol: felt) { + return ERC721.symbol(); +} + +@view +func balanceOf{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(owner: felt) -> ( + balance: Uint256 +) { + return ERC721.balance_of(owner); +} + +@view +func ownerOf{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( + token_id: Uint256 +) -> (owner: felt) { + return ERC721.owner_of(token_id); +} + +@view +func getApproved{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( + token_id: Uint256 +) -> (approved: felt) { + return ERC721.get_approved(token_id); +} + +@view +func isApprovedForAll{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( + owner: felt, operator: felt +) -> (isApproved: felt) { + let (isApproved) = ERC721.is_approved_for_all(owner, operator); + return (isApproved=isApproved); +} + +@view +func tokenURI{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( + tokenId: Uint256 +) -> (tokenURI: felt) { + let (tokenURI) = ERC721.token_uri(tokenId); + return (tokenURI=tokenURI); +} + +@view +func owner{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> (owner: felt) { + return Ownable.owner(); +} + +// +// Externals +// + +@external +func approve{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( + to: felt, tokenId: Uint256 +) { + ERC721.approve(to, tokenId); + return (); +} + +@external +func setApprovalForAll{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( + operator: felt, approved: felt +) { + ERC721.set_approval_for_all(operator, approved); + return (); +} + +@external +func transferFrom{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( + from_: felt, to: felt, tokenId: Uint256 +) { + ERC721.transfer_from(from_, to, tokenId); + return (); +} + +@external +func safeTransferFrom{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( + from_: felt, to: felt, tokenId: Uint256, data_len: felt, data: felt* +) { + ERC721.safe_transfer_from(from_, to, tokenId, data_len, data); + return (); +} + +@external +func transferOwnership{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( + newOwner: felt +) { + Ownable.transfer_ownership(newOwner); + return (); +} + +@external +func renounceOwnership{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() { + Ownable.renounce_ownership(); + return (); +} + +@external +func permissionedMint{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( + to: felt, tokenId: Uint256, data_len: felt, data: felt*, tokenURI: felt +) { + // Ownable.assert_only_owner(); + ERC721._safe_mint(to, tokenId, data_len, data); + ERC721._set_token_uri(tokenId, tokenURI); + return (); +} diff --git a/apps/blockchain/hardhat.config.ts b/apps/blockchain/hardhat.config.ts new file mode 100644 index 00000000..45540410 --- /dev/null +++ b/apps/blockchain/hardhat.config.ts @@ -0,0 +1,54 @@ +import { HardhatUserConfig } from "hardhat/types"; +import "@shardlabs/starknet-hardhat-plugin"; +import "@nomiclabs/hardhat-ethers"; +import "@nomiclabs/hardhat-etherscan"; +import chai from "chai"; +import { solidity } from "ethereum-waffle"; +import { config as dotenvConfig } from "dotenv"; + +dotenvConfig(); + +chai.use(solidity); + +const { + PRIVATE_KEY, + GOERLI_ALCHEMY_KEY, + HOSTNAME_L1, + HOSTNAME_L2, + ETHERSCAN_API_KEY, + GOERLI_PRIVATE_KEY, + MAINNET_ALCHEMY_KEY, +} = process.env; + +const config: HardhatUserConfig = { + solidity: "0.8.17", + starknet: { + venv: "active", + wallets: { + OpenZeppelin: { + accountName: "OpenZeppelin", + modulePath: + "starkware.starknet.wallets.open_zeppelin.OpenZeppelinAccount", + accountPath: "~/.starknet_accounts", + }, + }, + }, + networks: { + goerli: { + url: `https://eth-goerli.alchemyapi.io/v2/${GOERLI_ALCHEMY_KEY}`, + accounts: GOERLI_PRIVATE_KEY ? [GOERLI_PRIVATE_KEY] : [], + chainId: 5, + }, + l2_testnet: { + url: `http://${HOSTNAME_L2 || "localhost"}:5050`, + }, + l1_testnet: { + url: `http://${HOSTNAME_L1 || "localhost"}:8545`, + }, + }, + etherscan: { + apiKey: ETHERSCAN_API_KEY, + }, +}; + +export default config; diff --git a/apps/blockchain/lib/cairo_contracts b/apps/blockchain/lib/cairo_contracts new file mode 160000 index 00000000..331844dc --- /dev/null +++ b/apps/blockchain/lib/cairo_contracts @@ -0,0 +1 @@ +Subproject commit 331844dcf278ccdf96ce3b63fb3e5f2c78970561 diff --git a/apps/blockchain/lib/open_zeppelin b/apps/blockchain/lib/open_zeppelin new file mode 160000 index 00000000..331844dc --- /dev/null +++ b/apps/blockchain/lib/open_zeppelin @@ -0,0 +1 @@ +Subproject commit 331844dcf278ccdf96ce3b63fb3e5f2c78970561 diff --git a/apps/blockchain/package.json b/apps/blockchain/package.json new file mode 100644 index 00000000..411b7401 --- /dev/null +++ b/apps/blockchain/package.json @@ -0,0 +1,57 @@ +{ + "name": "@starklane/blockchain", + "description": "The Starklane NFT Bridge: seamless transfer of NFTs between ETH L1 & Starknet L2. Smart contracts, user-friendly interface, secure & efficient solution. Experience the future of NFT ownership today", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "devDependencies": { + "@ethersproject/abi": "^5.4.7", + "@ethersproject/providers": "^5.4.7", + "@openzeppelin/hardhat-upgrades": "^1.22.0", + "@shardlabs/starknet-hardhat-plugin": "^0.7.1", + "@typechain/ethers-v5": "^10.1.0", + "@typechain/hardhat": "^6.1.5", + "@types/chai": "^4.3.4", + "@types/mocha": "^10.0.0", + "@types/node": ">=12.0.0", + "chai": "^4.3.7", + "chai-bignumber": "^3.1.0", + "dotenv": "^16.0.3", + "ethers": "^5.4.7", + "hardhat": "^2.12.6", + "hardhat-gas-reporter": "^1.0.9", + "solidity-coverage": "^0.8.2", + "ts-node": "^10.9.1", + "typechain": "^8.1.0", + "typescript": ">=4.5.0" + }, + "scripts": { + "test": "hardhat --network l1_testnet test", + "deploy-l1:goerli": "hardhat run ./scripts/deploy-L1.ts --network goerli", + "deploy-l2:goerli": "hardhat run ./scripts/deploy-L2.ts --network goerli", + "deploy-account:goerli": "hardhat run ./scripts/deploy-account.ts --network goerli", + "build:l1": "hardhat compile", + "build:l2": "hardhat starknet-compile", + "new:account:goerli": "yarn hardhat starknet-new-account --starknet-network alpha-goerli --wallet OpenZeppelin", + "deploy:account:goerli": "yarn hardhat starknet-deploy-account --starknet-network alpha-goerli --wallet OpenZeppelin", + "lint": "npx prettier --check . && find contracts/ -iname '*.cairo' -exec ~/cairo_venv/bin/cairo-format -c {} +", + "lint:fix": "npx prettier --write . && find contracts/ -iname '*.cairo' -exec ~/cairo_venv/bin/cairo-format -i {} +", + "testnet:l2": "~/cairo_venv/bin/starknet-devnet --host 0.0.0.0 --lite-mode" + }, + "dependencies": { + "@nomiclabs/hardhat-ethers": "^2.2.2", + "@nomiclabs/hardhat-etherscan": "^3.1.5", + "@nomiclabs/hardhat-waffle": "^2.0.3", + "@openzeppelin/contracts": "^4.8.1", + "@openzeppelin/contracts-upgradeable": "^4.8.1", + "@shardlabs/starknet-hardhat-plugin": "^0.7.1", + "ethereum-waffle": "^3.4.4", + "lint-staged": "^13.1.0", + "prettier": "^2.8.3", + "prettier-plugin-solidity": "^1.1.1" + }, + "lint-staged": { + "*.{js,ts,sol}": "npx prettier --write", + "*.cairo": "cairo-format -i" + } +} diff --git a/apps/blockchain/scripts/deploy-L1.ts b/apps/blockchain/scripts/deploy-L1.ts new file mode 100644 index 00000000..97565016 --- /dev/null +++ b/apps/blockchain/scripts/deploy-L1.ts @@ -0,0 +1,40 @@ +import { ethers } from "hardhat"; +import hre from "hardhat"; + +async function main() { + const starknetCore = process.env.STARKNET_CORE_L1_ADDRESS || ""; + + console.log("=> starknetCore", starknetCore); + + // Deploy Bridge + const Bridge = await ethers.getContractFactory("Bridge"); + const bridge = await Bridge.deploy(starknetCore); + await bridge.deployed(); + console.log(`Bridge deployed to ${bridge.address}`); + + // Wait 5 validations before verifying + await bridge.deployTransaction.wait(5); + console.log(`Bridge validations complete`); + + // Verify Bridge on Etherscan + // TODO: remove try catch when https://github.com/NomicFoundation/hardhat/pull/3609 is merged + try { + await hre.run("verify:verify", { + address: bridge.address, + constructorArguments: [starknetCore], + }); + } catch (err: any) { + if (err.message.includes("Reason: Already Verified")) { + console.log("Contract is already verified!"); + } + } + console.log(`Bridge verified on Etherscan`); + process.exit(0); +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/apps/blockchain/scripts/deploy-L2.ts b/apps/blockchain/scripts/deploy-L2.ts new file mode 100644 index 00000000..39c3840c --- /dev/null +++ b/apps/blockchain/scripts/deploy-L2.ts @@ -0,0 +1,44 @@ +import * as dotenv from "dotenv"; +import { starknet } from "hardhat"; + +dotenv.config(); + +const maxFee = 5e16; + +const { L2_DEPLOYER_ADDRESS = "", L2_DEPLOYER_PRIVATE_KEY = "" } = process.env; + +export async function getOZAccount() { + return await starknet.OpenZeppelinAccount.getAccountFromAddress( + L2_DEPLOYER_ADDRESS, + L2_DEPLOYER_PRIVATE_KEY + ); +} + +async function main() { + if (!L2_DEPLOYER_ADDRESS || !L2_DEPLOYER_PRIVATE_KEY) { + throw new Error( + "Please set your L2 deployer private key & address in your .env file" + ); + } + console.log("Deploying L2 bridge..."); + console.log("L2 deployer address: ", L2_DEPLOYER_ADDRESS); + const contractFactory = await starknet.getContractFactory("bridge"); + console.log("Getting OpenZepplin Account..."); + const deployerAccount = await getOZAccount(); + console.log("Declaring hash..."); + const bridgeImplHash = await deployerAccount.declare(contractFactory, { + maxFee, + }); + console.log("L2 bridge class is declared at hash: ", bridgeImplHash); + console.log("Deploying contract..."); + const contract = await deployerAccount.deploy(contractFactory, {}); + console.log("Deployed to:", contract.address); + process.exit(0); +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/apps/blockchain/scripts/deploy-account.ts b/apps/blockchain/scripts/deploy-account.ts new file mode 100644 index 00000000..03278a97 --- /dev/null +++ b/apps/blockchain/scripts/deploy-account.ts @@ -0,0 +1,37 @@ +import { starknet } from "hardhat"; + +async function keypress() { + if (process.stdin.isTTY) { + process.stdin.setRawMode(true); + } + return new Promise((resolve) => + process.stdin.once("data", () => { + process.stdin.setRawMode(false); + resolve(); + }) + ); +} + +(async () => { + const account = await starknet.OpenZeppelinAccount.createAccount({ + salt: process.env.L2_DEPLOYER_SALT, + privateKey: process.env.L2_DEPLOYER_PRIVATE_KEY, + }); + console.log( + `Account created at ${account.address} with private key=${account.privateKey} and public key=${account.publicKey}` + ); + console.log( + "Please fund the address. Even after you get a confirmation that the funds were transferred, you may want to wait for a couple of minutes." + ); + console.log("Press any key to continue..."); + await keypress(); + console.log("Deploying..."); + await account.deployAccount({ maxFee: 1e18 }); + console.log("Deployed"); + process.exit(0); +})() + .then(() => process.exit(0)) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/apps/blockchain/tsconfig.json b/apps/blockchain/tsconfig.json new file mode 100644 index 00000000..e5f1a640 --- /dev/null +++ b/apps/blockchain/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + } +} diff --git a/apps/blockchain/utils/getSelector.py b/apps/blockchain/utils/getSelector.py new file mode 100644 index 00000000..8482e79c --- /dev/null +++ b/apps/blockchain/utils/getSelector.py @@ -0,0 +1,4 @@ +from starkware.starknet.compiler.compile import \ + get_selector_from_name + +print(get_selector_from_name('deposit')) diff --git a/apps/web/.env.example b/apps/web/.env.example new file mode 100644 index 00000000..17bf2494 --- /dev/null +++ b/apps/web/.env.example @@ -0,0 +1,3 @@ +ALCHEMY_API_KEY= +L2_NETWORK= +NEXT_PUBLIC_L1_BRIDGE_ADDRESS= \ No newline at end of file diff --git a/apps/web/.eslintrc.json b/apps/web/.eslintrc.json new file mode 100755 index 00000000..bffb357a --- /dev/null +++ b/apps/web/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100755 index 00000000..0d9ac5bb --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +/.yarn + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo diff --git a/apps/web/.prettierignore b/apps/web/.prettierignore new file mode 100644 index 00000000..6193d00f --- /dev/null +++ b/apps/web/.prettierignore @@ -0,0 +1,2 @@ +.next +.vscode \ No newline at end of file diff --git a/apps/web/.vscode/settings.json b/apps/web/.vscode/settings.json new file mode 100644 index 00000000..d0679104 --- /dev/null +++ b/apps/web/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true +} \ No newline at end of file diff --git a/apps/web/abi/abi.json b/apps/web/abi/abi.json new file mode 100644 index 00000000..988f2c1f --- /dev/null +++ b/apps/web/abi/abi.json @@ -0,0 +1,126 @@ +[ + { + "inputs": [ + { "internalType": "address", "name": "starknetCore_", "type": "address" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "inputs": [], + "name": "deposit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "l1_contract_address", + "type": "address" + }, + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "name": "forceWithdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "l2GatewayAddress", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "selector", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "name": "setL2GatewayAddress", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "name": "setSelector", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "starknetCore", + "outputs": [ + { + "internalType": "contract IStarknetMessaging", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "string", "name": "text", "type": "string" }], + "name": "strToUint", + "outputs": [ + { "internalType": "uint256", "name": "res", "type": "uint256" } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newOwner", "type": "address" } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/apps/web/app/head.tsx b/apps/web/app/head.tsx new file mode 100644 index 00000000..26cb4bcc --- /dev/null +++ b/apps/web/app/head.tsx @@ -0,0 +1,11 @@ +import { DefaultTags } from '#/ui/DefaultTags'; + +export default function Head() { + return ( + <> + + Starklane + + + ); +} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx new file mode 100644 index 00000000..485e28a7 --- /dev/null +++ b/apps/web/app/layout.tsx @@ -0,0 +1,50 @@ +'use client'; + +import '#/styles/globals.css'; + +import { WagmiConfig, createClient } from 'wagmi'; +import { goerli } from 'wagmi/chains'; +import { ConnectKitProvider, getDefaultClient } from 'connectkit'; +import { InjectedConnector, StarknetConfig } from '@starknet-react/core'; + +import Header from '#/ui/Header'; +import Footer from '#/ui/Footer'; + +const alchemyId = process.env.ALCHEMY_ID; +const chains = [goerli]; + +const wagmiClient = createClient( + getDefaultClient({ + appName: 'Your App Name', + alchemyId, + chains, + }), +); + +const connectors = [ + new InjectedConnector({ options: { id: 'braavos' } }), + new InjectedConnector({ options: { id: 'argentX' } }), +]; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + + + +
+
{children}
+