diff --git a/bridge-web/src/App.tsx b/bridge-web/src/App.tsx index 210393f..02a7485 100644 --- a/bridge-web/src/App.tsx +++ b/bridge-web/src/App.tsx @@ -86,14 +86,14 @@ function App() { const { data: fees } = useContractRead({ abi: tokenManagerAbi, functionName: "getFees", - address: fromChainConfig.tokenManagerAddress, - enabled: !!fromChainConfig.tokenManagerAddress, + address: token.tokenManagerAddress, + enabled: !!token.tokenManagerAddress, }); const { data: paused } = useContractRead({ abi: tokenManagerAbi, functionName: "paused", - address: fromChainConfig.tokenManagerAddress, - enabled: !!fromChainConfig.tokenManagerAddress, + address: token.tokenManagerAddress, + enabled: !!token.tokenManagerAddress, }); const { data: balance } = useContractRead({ abi: erc20ABI, @@ -108,9 +108,9 @@ function App() { abi: erc20ABI, functionName: "allowance", address: token.address, - args: [account!, fromChainConfig.tokenManagerAddress], + args: [account!, token.tokenManagerAddress], enabled: - !!account && !!token.address && !!fromChainConfig.tokenManagerAddress, + !!account && !!token.address && !!token.tokenManagerAddress, watch: true, }); @@ -124,7 +124,7 @@ function App() { : false; const { config: transferConfig } = usePrepareContractWrite({ - address: fromChainConfig.tokenManagerAddress, + address: token.tokenManagerAddress, abi: tokenManagerAbi, args: recipientEth && [ token.address, @@ -154,7 +154,7 @@ function App() { } = useContractWrite({ mode: "prepared", request: { - address: fromChainConfig.tokenManagerAddress, + address: token.tokenManagerAddress, chain: fromChainConfig.wagmiChain, account: account!, abi: tokenManagerAbi, @@ -176,7 +176,7 @@ function App() { address: token.address, abi: erc20ABI, args: [ - fromChainConfig.tokenManagerAddress, + token.tokenManagerAddress, amount ? parseUnits(amount, decimals ?? 0) : 0n, ], functionName: "approve", @@ -264,7 +264,7 @@ function App() { toast.update(id, { render: (
- Bridge txn complete, funds arrived to {toChainConfig.name}{" "} + Bridge txn complete, funds arrived on {toChainConfig.name}{" "} chain. View on{" "} > = zq: { chain: "zq", name: "Zilliqa", - tokenManagerAddress: "0x6D61eFb60C17979816E4cE12CD5D29054E755948", chainGatewayAddress: "0xbA44BC29371E19117DA666B729A1c6e1b35DDb40", - tokenManagerType: TokenManagerType.LockAndRelease, wagmiChain: zilliqa, tokens: [ { @@ -38,6 +37,8 @@ export const chainConfigs: Partial> = blockExplorer: "https://otterscan.zilliqa.com/address/0x63B991C17010C21250a0eA58C6697F696a48cdf3", logo: hrse_token, + tokenManagerAddress: "0x6D61eFb60C17979816E4cE12CD5D29054E755948", + tokenManagerType: TokenManagerType.LockAndRelease, }, { name: "FPS", @@ -45,6 +46,8 @@ export const chainConfigs: Partial> = blockExplorer: "https://otterscan.zilliqa.com/address/0x241c677D9969419800402521ae87C411897A029f", logo: fps_token, + tokenManagerAddress: "0x6D61eFb60C17979816E4cE12CD5D29054E755948", + tokenManagerType: TokenManagerType.LockAndRelease, }, ], chainId: 32769, @@ -59,9 +62,7 @@ export const chainConfigs: Partial> = bsc, `${import.meta.env.VITE_BSC_MAINNET_API}/${import.meta.env.VITE_BSC_MAINNET_KEY}`, ), - tokenManagerAddress: "0xF391A1Ee7b3ccad9a9451D2B7460Ac646F899f23", chainGatewayAddress: "0x3967f1a272Ed007e6B6471b942d655C802b42009", - tokenManagerType: TokenManagerType.MintAndBurn, tokens: [ { name: "HRSE", @@ -69,6 +70,8 @@ export const chainConfigs: Partial> = blockExplorer: "https://bscscan.com/address/0x3BE0E5EDC58bd55AAa381Fa642688ADC289c05a3", logo: hrse_token, + tokenManagerAddress: "0xF391A1Ee7b3ccad9a9451D2B7460Ac646F899f23", + tokenManagerType: TokenManagerType.MintAndBurn, }, { name: "FPS", @@ -76,6 +79,8 @@ export const chainConfigs: Partial> = blockExplorer: "https://bscscan.com/address/0x351dA1E7500aBA1d168b9435DCE73415718d212F", logo: fps_token, + tokenManagerAddress: "0xF391A1Ee7b3ccad9a9451D2B7460Ac646F899f23", + tokenManagerType: TokenManagerType.MintAndBurn, }, ], chainId: 56, @@ -88,8 +93,6 @@ export const chainConfigs: Partial> = "zq-testnet": { chain: "zq-testnet", name: "Zilliqa Testnet", - tokenManagerAddress: "0x1509988c41f02014aA59d455c6a0D67b5b50f129", - tokenManagerType: TokenManagerType.LockAndRelease, chainGatewayAddress: "0x7370e69565BB2313C4dA12F9062C282513919230", wagmiChain: zilliqaTestnet, tokens: [ @@ -99,6 +102,8 @@ export const chainConfigs: Partial> = blockExplorer: "https://otterscan.testnet.zilliqa.com/address/0x8618d39a8276D931603c6Bc7306af6A53aD2F1F3", logo: fps_token, + tokenManagerAddress: "0xF391A1Ee7b3ccad9a9451D2B7460Ac646F899f23", + tokenManagerType: TokenManagerType.LockAndRelease, }, { name: "TSLM Z", @@ -106,6 +111,8 @@ export const chainConfigs: Partial> = blockExplorer: "https://otterscan.testnet.zilliqa.com/address/0xE90Dd366D627aCc5feBEC126211191901A69f8a0", logo: test_hrse_token, + tokenManagerAddress: "0xF391A1Ee7b3ccad9a9451D2B7460Ac646F899f23", + tokenManagerType: TokenManagerType.LockAndRelease, }, ], chainId: 33101, @@ -120,8 +127,6 @@ export const chainConfigs: Partial> = bscTestnet, `${import.meta.env.VITE_BSC_TESTNET_API}/${import.meta.env.VITE_BSC_TESTNET_KEY}`, ), - tokenManagerAddress: "0xA6D73210AF20a59832F264fbD991D2abf28401d0", - tokenManagerType: TokenManagerType.MintAndBurn, chainGatewayAddress: "0xa9A14C90e53EdCD89dFd201A3bF94D867f8098fE", tokens: [ { @@ -130,6 +135,8 @@ export const chainConfigs: Partial> = blockExplorer: "https://testnet.bscscan.com/address/0x5190e8b4Bbe8C3a732BAdB600b57fD42ACbB9F4B", logo: fps_token, + tokenManagerAddress: "0xA6D73210AF20a59832F264fbD991D2abf28401d0", + tokenManagerType: TokenManagerType.MintAndBurn, }, { name: "TSLM B", @@ -137,6 +144,17 @@ export const chainConfigs: Partial> = blockExplorer: "https://testnet.bscscan.com/address/0x7Cc585de659E8938Aa7d5709BeaF34bD108bdC03", logo: test_hrse_token, + tokenManagerAddress: "0xA6D73210AF20a59832F264fbD991D2abf28401d0", + tokenManagerType: TokenManagerType.MintAndBurn, + }, + { + name: "zbBSCERC20", + address: "0x59A23d0957B63BC6c5682F211eE731513EECBB98", + blockExplorer: + "https://testnet.bscscan.com/address/0x59A23d0957B63BC6c5682F211eE731513EECBB98", + logo: fps_token, + tokenManagerAddress: "0x36b8A9cd6Bf9bfA5984093005cf81CAfB1Bf06F7", + tokenManagerType: TokenManagerType.ZilBridge }, ], chainId: 97, @@ -150,9 +168,7 @@ export type ChainConfig = { name: string; chain: Chains; wagmiChain: Chain; - tokenManagerAddress: `0x${string}`; chainGatewayAddress: `0x${string}`; - tokenManagerType: TokenManagerType; tokens: TokenConfig[]; chainId: number; isZilliqa: boolean; @@ -165,4 +181,6 @@ export type TokenConfig = { address: `0x${string}`; blockExplorer: string; logo?: string; + tokenManagerAddress: `0x${string}`; + tokenManagerType: TokenManagerType; }; diff --git a/docs/zilbridge.md b/docs/zilbridge.md index a431802..f96378e 100644 --- a/docs/zilbridge.md +++ b/docs/zilbridge.md @@ -122,6 +122,12 @@ export TOKEN_MANAGER=(whatever address the deployNativeTokenManagerV3 script abo npx hardhat run scripts/deploy.ts ``` +And now we ship an ERC20 proxy for our ZRC2 and switcheo tokens. + +``` + +``` + And now we can set up routing for the tokens we just deployed. This is "just" calls, so ``` diff --git a/scilla-contracts/scripts/deploy.ts b/scilla-contracts/scripts/deploy.ts index 5a25c80..1288d47 100644 --- a/scilla-contracts/scripts/deploy.ts +++ b/scilla-contracts/scripts/deploy.ts @@ -15,14 +15,14 @@ async function main() { } let tokenContract = await hre.deployScillaContract("SwitcheoTokenZRC2", "Bridged-XTST", "XTST", account.address, 18, 0, zqTestnetTokenManager); - console.log(`TokenContract: ${tokenContract.address}`); + console.log(`zq_bridged_erc20 = ${tokenContract.address}`); let nativeBridgedContract = await hre.deployScillaContract("SwitcheoTokenZRC2", "Bridged-BNB", "eBNB", account.address, 18, 0, zqTestnetTokenManager); - console.log(`NativeBridgedContract: ${nativeBridgedContract.address}`); + console.log(`zq_bridged_bnb = ${nativeBridgedContract.address}`); let local = await hre.deployScillaContract("FungibleToken", account.address, "zq_native Test", "ZTST", 18, 1_000_000); - console.log(`Locally generated fungible token: ${local.address}`); + console.log(`zq_zrc2 = ${local.address}`); } // We recommend this pattern to be able to use async/await everywhere diff --git a/smart-contracts/script/testnet_config.s.sol b/smart-contracts/script/testnet_config.s.sol index b77e10e..0c1d8fc 100644 --- a/smart-contracts/script/testnet_config.s.sol +++ b/smart-contracts/script/testnet_config.s.sol @@ -20,8 +20,14 @@ abstract contract TestnetConfig { address public constant zq_chainGateway = 0x7370e69565BB2313C4dA12F9062C282513919230; address public constant zq_lockAndReleaseOrNativeTokenManager = 0xBe90AB2cd65E207F097bEF733F8D239A59698b8A; + // Scilla contracts. address public constant zq_bridged_erc20 = address(0x00839901f1e39De75301667C6bBbF7fB556Ea2510E); address public constant zq_bridged_bnb = address(0x0006852e68A3c24917cfA4C2dbDaE4B308C69aDA5e); address public constant zq_zrc2 = address(0x00155F0f76b660290F2F00Bb5674b80eDC208bF2e6); + // ERC20 fascias for Scilla contracts + address public constant zq_bridged_erc20_evm = address(0x0); + address public constant zq_bridged_bnb_evm = address(0x0); + address public constant zq_zrc2_evm = address(0x0); + } diff --git a/smart-contracts/script/zq-testnet/deployZRC2ERC20.s.sol b/smart-contracts/script/zq-testnet/deployZRC2ERC20.s.sol new file mode 100644 index 0000000..51ba774 --- /dev/null +++ b/smart-contracts/script/zq-testnet/deployZRC2ERC20.s.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.20; + +import {Script} from "forge-std/Script.sol"; +import {LockAndReleaseTokenManagerUpgradeable} from "contracts/periphery/LockAndReleaseTokenManagerUpgradeable.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { ZRC2ProxyForZRC2 } from "test/zilbridge/zrc2erc20/ZRC2ProxyForZRC2.sol"; +import { TestnetConfig } from "script/testnet_config.s.sol"; +import "forge-std/console.sol"; + +/*** @title Deploy an ERC20 proxy for our ZRC2, so we can set routing with it. + */ +contract Deployment is Script, TestnetConfig { + function run() external { + uint256 validatorPrivateKey = vm.envUint("PRIVATE_KEY_TESTNET"); + address validator = vm.addr(validatorPrivateKey); + console.log("Owner is %s", validator); + vm.startBroadcast(validatorPrivateKey); + { + ZRC2ProxyForZRC2 proxy = new ZRC2ProxyForZRC2(zq_bridged_erc20); + console.log("zq_bridged_erc20_evm = %s", address(proxy)); + } + { + ZRC2ProxyForZRC2 proxy = new ZRC2ProxyForZRC2(zq_bridged_bnb); + console.log("zq_bridged_bnb_evm = %s", address(proxy)); + } + { + ZRC2ProxyForZRC2 proxy = new ZRC2ProxyForZRC2(zq_zrc2); + console.log("zq_zrc2_evm = %s", address(proxy)); + } + } +} diff --git a/smart-contracts/script/zq-testnet/registerZilBridgeTokens.s.sol b/smart-contracts/script/zq-testnet/registerZilBridgeTokens.s.sol deleted file mode 100644 index eb95a8b..0000000 --- a/smart-contracts/script/zq-testnet/registerZilBridgeTokens.s.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity ^0.8.20; - -import {Script} from "forge-std/Script.sol"; -import {LockAndReleaseTokenManagerUpgradeable} from "contracts/periphery/LockAndReleaseTokenManagerUpgradeable.sol"; -import {ITokenManagerStructs} from "contracts/periphery/TokenManagerUpgradeable.sol"; -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "forge-std/console.sol"; - -/*** @notice this script wires up the token manager for the zilbridge tests on zq_testnet. The corresponding code on bsc is - * in - */ diff --git a/smart-contracts/test/zilbridge/zrc2erc20/ScillaConnector.sol b/smart-contracts/test/zilbridge/zrc2erc20/ScillaConnector.sol new file mode 100644 index 0000000..63f86ca --- /dev/null +++ b/smart-contracts/test/zilbridge/zrc2erc20/ScillaConnector.sol @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.20; + +library ScillaConnector { + uint private constant CALL_SCILLA_WITH_THE_SAME_SENDER = 1; + uint private constant SCILLA_CALL_PRECOMPILE_ADDRESS = 0x5a494c53; + uint private constant SCILLA_STATE_READ_PRECOMPILE_ADDRESS = 0x5a494c92; + + /** + * @dev Calls a ZRC2 contract function with two arguments + * @param target The address of the ZRC2 contract + * @param tran_name The name of the function to call + * @param arg1 The first argument to the function + * @param arg2 The second argument to the function + */ + function call( + address target, + string memory tran_name, + address arg1, + uint128 arg2 + ) internal { + bytes memory encodedArgs = abi.encode( + target, + tran_name, + CALL_SCILLA_WITH_THE_SAME_SENDER, + arg1, + arg2 + ); + uint256 argsLength = encodedArgs.length; + + assembly { + let alwaysSuccessForThisPrecompile := call( + 21000, + SCILLA_CALL_PRECOMPILE_ADDRESS, + 0, + add(encodedArgs, 0x20), + argsLength, + 0x20, + 0 + ) + } + } + + /** + * @dev Calls a ZRC2 contract function with three arguments + * @param target The address of the ZRC2 contract + * @param tran_name The name of the function to call on the ZRC2 contract + * @param arg1 The first argument to the function + * @param arg2 The second argument to the function + * @param arg3 The third argument to the function + */ + function call( + address target, + string memory tran_name, + address arg1, + address arg2, + uint128 arg3 + ) internal { + bytes memory encodedArgs = abi.encode( + target, + tran_name, + CALL_SCILLA_WITH_THE_SAME_SENDER, + arg1, + arg2, + arg3 + ); + uint256 argsLength = encodedArgs.length; + + assembly { + let alwaysSuccessForThisPrecompile := call( + 21000, + SCILLA_CALL_PRECOMPILE_ADDRESS, + 0, + add(encodedArgs, 0x20), + argsLength, + 0x20, + 0 + ) + } + } + + /** + * @dev Reads a 128 bit integer from a ZRC2 contract + * @param target The address of the ZRC2 contract + * @param variable_name The name of the variable to read from the ZRC2 contract + * @return The value of the variable + */ + function readUint128( + address target, + string memory variable_name + ) internal view returns (uint128) { + bytes memory encodedArgs = abi.encode(target, variable_name); + uint256 argsLength = encodedArgs.length; + bytes memory output = new bytes(36); + + assembly { + let alwaysSuccessForThisPrecompile := staticcall( + 21000, + SCILLA_STATE_READ_PRECOMPILE_ADDRESS, + add(encodedArgs, 0x20), + argsLength, + add(output, 0x20), + 32 + ) + } + + return abi.decode(output, (uint128)); + } + + /** + * @dev Reads a 32 bit integer from a ZRC2 contract + * @param target The address of the ZRC2 contract + * @param variable_name The name of the variable to read from the ZRC2 contract + * @return The value of the variable + */ + function readUint32( + address target, + string memory variable_name + ) internal view returns (uint32) { + bytes memory encodedArgs = abi.encode(target, variable_name); + uint256 argsLength = encodedArgs.length; + bytes memory output = new bytes(36); + + assembly { + let alwaysSuccessForThisPrecompile := staticcall( + 21000, + SCILLA_STATE_READ_PRECOMPILE_ADDRESS, + add(encodedArgs, 0x20), + argsLength, + add(output, 0x20), + 32 + ) + } + + return abi.decode(output, (uint32)); + } + + /** + * @dev Reads a string from a ZRC2 contract + * @param target The address of the ZRC2 contract + * @param variable_name The name of the variable to read from the ZRC2 contract + * @return retVal The value of the variable + */ + function readString( + address target, + string memory variable_name + ) internal view returns (string memory retVal) { + bytes memory encodedArgs = abi.encode(target, variable_name); + uint256 argsLength = encodedArgs.length; + bool success; + bytes memory output = new bytes(128); + uint256 output_len = output.length - 4; + assembly { + success := staticcall( + 21000, + SCILLA_STATE_READ_PRECOMPILE_ADDRESS, + add(encodedArgs, 0x20), + argsLength, + add(output, 0x20), + output_len + ) + } + require(success); + + (retVal) = abi.decode(output, (string)); + return retVal; + } + + /** + * @dev Reads a 128 bit integer from a map in a ZRC2 contract + * @param variable_name The name of the map in the ZRC2 contract + * @param addressMapKey The key to the map + * @return The value associated with the key in the map + */ + function readMapUint128( + address target, + string memory variable_name, + address addressMapKey + ) internal view returns (uint128) { + bytes memory encodedArgs = abi.encode( + target, + variable_name, + addressMapKey + ); + uint256 argsLength = encodedArgs.length; + bytes memory output = new bytes(36); + + assembly { + let alwaysSuccessForThisPrecompile := staticcall( + 21000, + SCILLA_STATE_READ_PRECOMPILE_ADDRESS, + add(encodedArgs, 0x20), + argsLength, + add(output, 0x20), + 32 + ) + } + + return abi.decode(output, (uint128)); + } + + /** + * @dev Reads a 128 bit integer from a nested map in a ZRC2 contract + * @param target The address of the ZRC2 contract + * @param variable_name The name of the map in the ZRC2 contract + * @param firstMapKey The first key to the map + * @param secondMapKey The second key to the map + * @return The value associated with the keys in the map + */ + function readNestedMapUint128( + address target, + string memory variable_name, + address firstMapKey, + address secondMapKey + ) internal view returns (uint128) { + bytes memory encodedArgs = abi.encode( + target, + variable_name, + firstMapKey, + secondMapKey + ); + uint256 argsLength = encodedArgs.length; + bytes memory output = new bytes(36); + + assembly { + let alwaysSuccessForThisPrecompile := staticcall( + 21000, + SCILLA_STATE_READ_PRECOMPILE_ADDRESS, + add(encodedArgs, 0x20), + argsLength, + add(output, 0x20), + 32 + ) + } + + return abi.decode(output, (uint128)); + } +} diff --git a/smart-contracts/test/zilbridge/zrc2erc20/ZRC2ProxyForZRC2.sol b/smart-contracts/test/zilbridge/zrc2erc20/ZRC2ProxyForZRC2.sol new file mode 100644 index 0000000..4332aeb --- /dev/null +++ b/smart-contracts/test/zilbridge/zrc2erc20/ZRC2ProxyForZRC2.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {ScillaConnector} from "./ScillaConnector.sol"; + +contract ZRC2ProxyForZRC2 is IERC20 { + using ScillaConnector for address; + using SafeCast for uint256; + + address public zrc2_proxy; + + // Additional variables useful for wallets + uint8 public decimals; + string public symbol; + string public name; + + /** + * @notice Constructs a new ZRC2Proxy contract + * @param zrc2_address The address of the underlying ZRC2 contract + */ + constructor(address zrc2_address) { + zrc2_proxy = zrc2_address; + + symbol = zrc2_proxy.readString("symbol"); + decimals = uint256(zrc2_proxy.readUint32("decimals")).toUint8(); + name = zrc2_proxy.readString("name"); + } + + /** + * @notice Get the total supply of tokens + * @return The total supply of tokens + */ + function totalSupply() external view returns (uint256) { + return zrc2_proxy.readUint128("total_supply"); + } + + /** + * @notice Get the token balance for a specific account + * @param tokenOwner The address of the account + * @return The balance of the account + */ + function balanceOf(address tokenOwner) external view returns (uint256) { + return zrc2_proxy.readMapUint128("balances", tokenOwner); + } + + /** + * @notice Transfer tokens to a specified address + * @param to The address to transfer to + * @param tokens The amount of tokens to transfer + * @return true if transfer was successful + */ + function transfer(address to, uint256 tokens) external returns (bool) { + zrc2_proxy.call("Transfer", to, tokens.toUint128()); + return true; + } + + /** + * @notice Transfer tokens from one address to another + * @param from The address to transfer from + * @param to The address to transfer to + * @param tokens The amount of tokens to transfer + * @return true if transfer was successful + */ + function transferFrom( + address from, + address to, + uint256 tokens + ) external returns (bool) { + zrc2_proxy.call("TransferFrom", from, to, tokens.toUint128()); + return true; + } + + /** + * @notice Check the amount of tokens that an owner has allowed a spender to use + * @param tokenOwner The address of the token owner + * @param spender The address of the spender + * @return The amount of tokens remaining for the spender + */ + function allowance( + address tokenOwner, + address spender + ) external view returns (uint256) { + return + zrc2_proxy.readNestedMapUint128("allowances", tokenOwner, spender); + } + + /** + * @notice Approve a spender to spend a certain amount of tokens + * @param spender The address of the spender + * @param new_allowance The new allowance for the spender + * @return true if approval was successful + */ + function approve( + address spender, + uint256 new_allowance + ) external returns (bool) { + uint256 current_allowance = this.allowance(msg.sender, spender); + + if (current_allowance >= new_allowance) { + zrc2_proxy.call( + "DecreaseAllowance", + spender, + (current_allowance - new_allowance).toUint128() + ); + } else { + zrc2_proxy.call( + "IncreaseAllowance", + spender, + (new_allowance - current_allowance).toUint128() + ); + } + return true; + } +}