Skip to content

Commit e256823

Browse files
authored
feat(protocol-contracts): add bnb configs (#1796)
* feat(protocol-contracts): add bnb configs * feat(protocol-contracts): first part of bnb runbook * chore(protocol-contracts): update runbook * chore(protocol-contracts): update BNB OFT runbook * docs(protocol-contracts): fix oft send commands * chore(protocol-contracts): minor improvements following comments * chore(protocol-contracts): fix typo in bnb runbook
1 parent 11b613b commit e256823

File tree

10 files changed

+446
-2
lines changed

10 files changed

+446
-2
lines changed

protocol-contracts/token/.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ PRIVATE_KEY=
33
# mainnet
44
MAINNET_RPC_URL=
55
RPC_URL_ZAMA_GATEWAY_MAINNET=
6+
RPC_URL_BNB_MAINNET=
67
# testnet
78
SEPOLIA_RPC_URL=
89
RPC_URL_ZAMA_GATEWAY_TESTNET=
10+
RPC_URL_BNB_TESTNET=
911
# API for contract verification
1012
ETHERSCAN_API=TA5XXXXXXXXXXXXXXXXXXXXXXXXXXXGX9
1113
BLOCKSCOUT_API=https://explorer-xxxx-xxxxxxx.conduit.xyz/api # don't forget the /api suffix at the end
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# How to add ZamaOFT on BNB Chain
2+
3+
Currently, we have `ZamaERC20` and `ZamaOFTAdapter` deployed on Ethereum mainnet, and `ZamaOFT` deployed on Gateway mainnet. The `ZamaOFTAdapter` contract's owner and delegate are already setup to be an Aragon DAO contract.
4+
5+
The goal of this runbook is to guide you step by step on how to deploy a `ZamaOFT` instance on BNB Chain, and how to wire it to the already deployed `ZamaOFTAdapter` on Ethereum, via the Aragon DAO. We only add a single bidirectional pathway: `BNB <-> Ethereum` (i.e there is no step to wire `BNB <-> Gateway`).
6+
7+
## Step 1 : Recreating deployments
8+
9+
The first step is to recreate deployments artifacts for the already deployed contract. This is inspired from the official [LayerZero V2 docs](https://docs.layerzero.network/v2/tools/create-lz-oapp-cli/recreating-deployments).
10+
11+
First, run `pnpm install` and setup your `.env` file with all required values (see [`.env.example`](./.env.example)).
12+
13+
Create a `/deployments` folder in the root of the [/token] directory. The eventual structure would look like this:
14+
15+
```
16+
/deployments
17+
/ethereum-mainnet
18+
.chainId
19+
ZamaERC20.json
20+
ZamaOFTAdapter.json
21+
/gateway-mainnet
22+
.chainId
23+
ZamaOFT.json
24+
```
25+
26+
`/deployments/ethereum-mainnet/.chainId` - this file should contain the chain ID for the network, i.e `1` for Ethereum mainnet.
27+
28+
`/deployments/ethereum-mainnet/ZamaERC20.json` - for now, the only key that is necessary in the JSON file is `address`, note that the `abi` key is also necessary but this will be added via a script at the end of current step. Insert your ERC20 address into the address field.
29+
30+
```
31+
{
32+
"address": "<ZamaERC20Address>"
33+
}
34+
```
35+
36+
Follow the same similar steps for the remaining files, i.e `/deployments/ethereum-mainnet/ZamaOFTAdapter.json`, `/deployments/gateway-mainnet/.chainId` and `/deployments/gateway-mainnet/ZamaOFT.json`.
37+
38+
Then modifiy `hardhat.config.ts` by replacing `0x0` by the `ZamaERC20` address under the `ethereum-mainnet` field:
39+
40+
```typescript
41+
oftAdapter: {
42+
tokenAddress: '0x0', // Replace `0x0` with the address of the ERC20 token you want to adapt to the OFT functionality.
43+
}
44+
```
45+
46+
Then, run `npx hardhat compile` to ensure relevant artifacts that are required by Hardhat helper tasks involving the EVM OFT are generated.
47+
48+
Finally, in order to copy all the `abi` values in the `deployments` artifacts from the compilation artifacts located in `artifacts` folder, just run:
49+
50+
```
51+
npx ts-node scripts/copyAbiToDeployments.ts
52+
```
53+
54+
## Step 2 : Deploy ZamaOFT on BNB Chain
55+
56+
Run `npx hardhat deploy --network bnb-mainnet --tags ZamaOFT` command.
57+
58+
You can then verify the contract on bscscan by running `pnpm verify:etherscan:bnb:mainnet`.
59+
60+
## Step 3 : Wire the BNB OFT to Ethereum OFTAdapter
61+
62+
This can be done easily, since your deployer hot wallet is still the owner and delegate of the `ZamaOFT` instance on BNB Chain - later, after full wiring on both chains, ownership and delegate roles should be transferred to governance on BNB Chain, which should be a Safe multisig deployed on BNB mainnet.
63+
64+
You just have to run:
65+
66+
```npx hardhat lz:oapp:wire --oapp-config layerzero.config.mainnet.bnb.ts --skip-connections-from-eids <EID_ETHEREUM_V2_MAINNET>,<EID_ZAMA_V2_MAINNET>```
67+
68+
In previous command, replace `<EID_ETHEREUM_V2_MAINNET>` by `30101` and `EID_ZAMA_V2_MAINNET` by `30397`.
69+
70+
## Step 4 : Wire the Ethereum OFTAdapter to BNB OFT
71+
72+
This step is more complex, since the delegate of the OFTAdapter is an Aragon DAO, i.e it requires creating, approving and executing a DAO proposal via the Aragon DAO.
73+
74+
First, create an `ethereum-wiring.json` file containing the different transactions needed to be done, by running:
75+
76+
```
77+
npx hardhat lz:oapp:wire --oapp-config layerzero.config.mainnet.bnb.ts --output-filename ethereum-wiring.json
78+
```
79+
80+
When running previous command, select **no** when requested if you would you like to submit the required transactions (otherwise it would fail anyways). You should now have generated a new `ethereum-wiring.json` file in the root of the directory.
81+
82+
Now, run:
83+
84+
```
85+
npx ts-node scripts/convertToAragonProposal.ts ethereum-wiring.json aragonProposal.json
86+
```
87+
88+
This will convert the `ethereum-wiring.json` file to a new `aragonProposal.json` which could be directly uploaded inside the Aragon App UI, when creating an Aragon proposal, to streamling the process.
89+
90+
More precisely, in Aragon App, when you reach "Step 2 of 3" of proposal creation, click on the `Upload` button there, in select the newly created `aragonProposal.json` file to upload it and create the wiring proposal on Ethereum.
91+
92+
![Aragon Proposal Upload](images/AragonUpload.png)
93+
94+
After voting and execution of the wiring proposal, your OFT is now successfully setup.
95+
96+
## Step 5 : Test OFT transfers
97+
98+
You can test that the OFT BNB has been correctly wired by sending some amount of tokens from Ethereum to BNB Chain, and the other way around, by using commands such as:
99+
100+
```
101+
npx hardhat lz:oft:send --src-eid 30101 --dst-eid 30102 --amount 0.1 --to <RECEIVER_ADDRESS> --oapp-config layerzero.config.mainnet.bnb.ts
102+
```
103+
104+
to send 0.1 Zama token from Ethereum to BNB mainnet, and:
105+
106+
```
107+
npx hardhat lz:oft:send --src-eid 30102 --dst-eid 30101 --amount 0.1 --to <RECEIVER_ADDRESS> --oapp-config layerzero.config.mainnet.bnb.ts
108+
```
109+
110+
to send 0.1 Zama token from BNB mainnet to Ethereum.
111+
112+
## Step 6 : transfer delegate and owner
113+
114+
Once the transfer tests are successful, don't forget to transfer the delegate and owners roles of the BNB OFT instance to governance (i.e BNB Safe Multisig).
115+
116+
Those `cast` commands are helpful for transferring roles:
117+
118+
To get current OFT owner address:
119+
120+
```
121+
cast call <BNB_OFT_ADDRESS> "owner()(address)" --rpc-url <BNB_RPC_URL>
122+
```
123+
124+
To get current `EndpointV2` address:
125+
126+
```
127+
cast call <BNB_OFT_ADDRESS> "endpoint()(address)" --rpc-url <BNB_RPC_URL>
128+
```
129+
130+
To get current delegate:
131+
132+
```
133+
cast call <LZ_ENDPOINT_V2_ADDRESS> "delegates(address)(address)" <BNB_OFT_ADDRESS> --rpc-url <BNB_RPC_URL>
134+
```
135+
136+
To transfer delegate role:
137+
138+
```
139+
cast send <BNB_OFT_ADDRESS> "setDelegate(address)" <BNB_SAFE_ADDRESS> --rpc-url <BNB_RPC_URL> --private-key <DEPLOYER_PRIVATE_KEY>
140+
```
141+
142+
To transfer owner role:
143+
144+
```
145+
cast send <BNB_OFT_ADDRESS> "transferOwnership(address)" <BNB_SAFE_ADDRESS> --rpc-url <BNB_RPC_URL> --private-key <DEPLOYER_PRIVATE_KEY>
146+
```

protocol-contracts/token/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ If you want to send tokens from/to other chains, you can directly use `lz:oft:se
116116
For example you can send `1.5` ZAMA token from Ethereum Sepolia to Gateway Testnet from the deployer wallet to a custom receiver address by running this command and following instructions:
117117

118118
```bash
119-
npx hardhat lz:oft:send --src-eid 40161 --dst-eid 40424 --amount 1.5 --to <RECEIVER_ADDRESS>
119+
npx hardhat lz:oft:send --src-eid 40161 --dst-eid 40424 --amount 1.5 --to <RECEIVER_ADDRESS> --oapp-config layerzero.config.testnet.ts
120120
```
121121

122122
**Note** In the OFTAdapter case here, contrarily to the OFT case, 2 transactions are sent in previous script instead of 1, because the sender must first approve the corresponding amount of the ERC20 token to the OFTAdapter (i.e calling approve method on the ERC20 method and passing the OFTAdapter address and correct amount as parameters), before locking them to the OFTAdapter contract in a second `send` transaction on OFTAdapter (reminder: in the OFT case, initiating a token transfer happens by directly burning an amount of the OFT contract by calling the `send` method of the OFT contract).

protocol-contracts/token/hardhat.config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@ const config: HardhatUserConfig = {
9191
url: process.env.RPC_URL_ZAMA_GATEWAY_TESTNET || '',
9292
accounts,
9393
},
94+
'bnb-mainnet': {
95+
eid: EndpointId.BSC_V2_MAINNET,
96+
url: process.env.RPC_URL_BNB_MAINNET || '',
97+
accounts,
98+
},
99+
'bnb-testnet': {
100+
eid: EndpointId.BSC_V2_TESTNET,
101+
url: process.env.RPC_URL_BNB_TESTNET || '',
102+
accounts,
103+
},
94104
hardhat: {
95105
// Need this for testing because TestHelperOz5.sol is exceeding the compiled contract size limit
96106
allowUnlimitedContractSize: true,
40 KB
Loading
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { EndpointId } from '@layerzerolabs/lz-definitions'
2+
import { ExecutorOptionType } from '@layerzerolabs/lz-v2-utilities'
3+
import { TwoWayConfig, generateConnectionsConfig } from '@layerzerolabs/metadata-tools'
4+
import { OAppEnforcedOption } from '@layerzerolabs/toolbox-hardhat'
5+
6+
import type { OmniPointHardhat } from '@layerzerolabs/toolbox-hardhat'
7+
8+
const ethereumContract: OmniPointHardhat = {
9+
eid: EndpointId.ETHEREUM_V2_MAINNET,
10+
contractName: 'ZamaOFTAdapter',
11+
}
12+
13+
const zamaMainnetContract: OmniPointHardhat = {
14+
eid: EndpointId.ZAMA_V2_MAINNET,
15+
contractName: 'ZamaOFT',
16+
}
17+
18+
const bnbContract: OmniPointHardhat = {
19+
eid: EndpointId.BSC_V2_MAINNET,
20+
contractName: 'ZamaOFT',
21+
}
22+
23+
// We need the following pathways:
24+
// ZamaGatewayMainnet <-> Ethereum
25+
// BNB <-> Ethereum
26+
27+
// For this example's simplicity, we will use the same enforced options values for sending to all chains
28+
// For production, you should ensure `gas` is set to the correct value through profiling the gas usage of calling OFT._lzReceive(...) on the destination chain
29+
// To learn more, read https://docs.layerzero.network/v2/concepts/applications/oapp-standard#execution-options-and-enforced-settings
30+
const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [
31+
{
32+
msgType: 1,
33+
optionType: ExecutorOptionType.LZ_RECEIVE,
34+
gas: 80000,
35+
value: 0,
36+
},
37+
]
38+
39+
// With the config generator, pathways declared are automatically bidirectional
40+
// i.e. if you declare A,B there's no need to declare B,A
41+
const pathways: TwoWayConfig[] = [
42+
[
43+
ethereumContract, // Chain A contract
44+
zamaMainnetContract, // Chain B contract
45+
// TODO: Add custom ZAMA DVN in next line?
46+
[['LayerZero Labs'], [['Nethermind', 'Luganodes', 'P2P'], 2]], // [ requiredDVN[], [ optionalDVN[], threshold ] ]
47+
[15, 20], // [A to B confirmations, B to A confirmations]
48+
[EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain B enforcedOptions, Chain A enforcedOptions
49+
],
50+
[
51+
ethereumContract, // Chain A contract
52+
bnbContract, // Chain B contract
53+
// TODO: Add custom ZAMA DVN in next line?
54+
[['LayerZero Labs'], [['Nethermind', 'Luganodes', 'P2P'], 2]], // [ requiredDVN[], [ optionalDVN[], threshold ] ]
55+
[15, 20], // [A to B confirmations, B to A confirmations]
56+
[EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain B enforcedOptions, Chain A enforcedOptions
57+
],
58+
]
59+
60+
export default async function () {
61+
// Generate the connections config based on the pathways
62+
const connections = await generateConnectionsConfig(pathways)
63+
return {
64+
contracts: [{ contract: zamaMainnetContract }, { contract: ethereumContract }, { contract: bnbContract }],
65+
connections,
66+
}
67+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { EndpointId } from '@layerzerolabs/lz-definitions'
2+
import { ExecutorOptionType } from '@layerzerolabs/lz-v2-utilities'
3+
import { TwoWayConfig, generateConnectionsConfig } from '@layerzerolabs/metadata-tools'
4+
import { OAppEnforcedOption } from '@layerzerolabs/toolbox-hardhat'
5+
6+
import type { OmniPointHardhat } from '@layerzerolabs/toolbox-hardhat'
7+
8+
const sepoliaContract: OmniPointHardhat = {
9+
eid: EndpointId.SEPOLIA_V2_TESTNET,
10+
contractName: 'ZamaOFTAdapter',
11+
}
12+
13+
const zamaTestnetContract: OmniPointHardhat = {
14+
eid: EndpointId.ZAMA_V2_TESTNET,
15+
contractName: 'ZamaOFT',
16+
}
17+
18+
const bnbTestnetContract: OmniPointHardhat = {
19+
eid: EndpointId.BSC_V2_TESTNET,
20+
contractName: 'ZamaOFT',
21+
}
22+
23+
// We need the following pathways:
24+
// ZamaGatewayTestnet <-> Sepolia
25+
// BNBTestnet <-> Sepolia
26+
27+
// For this example's simplicity, we will use the same enforced options values for sending to all chains
28+
// For production, you should ensure `gas` is set to the correct value through profiling the gas usage of calling OFT._lzReceive(...) on the destination chain
29+
// To learn more, read https://docs.layerzero.network/v2/concepts/applications/oapp-standard#execution-options-and-enforced-settings
30+
const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [
31+
{
32+
msgType: 1,
33+
optionType: ExecutorOptionType.LZ_RECEIVE,
34+
gas: 50000,
35+
value: 0,
36+
},
37+
]
38+
39+
// With the config generator, pathways declared are automatically bidirectional
40+
// i.e. if you declare A,B there's no need to declare B,A
41+
const pathways: TwoWayConfig[] = [
42+
[
43+
sepoliaContract, // Chain A contract
44+
zamaTestnetContract, // Chain B contract
45+
// TODO: Add custom ZAMA DVN in next line?
46+
[['LayerZero Labs'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ]
47+
[15, 20], // [A to B confirmations, B to A confirmations]
48+
[EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain B enforcedOptions, Chain A enforcedOptions
49+
],
50+
[
51+
sepoliaContract,
52+
bnbTestnetContract,
53+
[['LayerZero Labs'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ]
54+
[15, 20], // [A to B confirmations, B to A confirmations]
55+
[EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain B enforcedOptions, Chain A enforcedOptions
56+
],
57+
]
58+
59+
export default async function () {
60+
// Generate the connections config based on the pathways
61+
const connections = await generateConnectionsConfig(pathways)
62+
return {
63+
contracts: [{ contract: zamaTestnetContract }, { contract: sepoliaContract }, { contract: bnbTestnetContract }],
64+
connections,
65+
}
66+
}

protocol-contracts/token/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"lint:js": "eslint '**/*.{js,ts,json}' && prettier --check .",
1212
"lint:sol": "solhint 'contracts/**/*.sol'",
1313
"test": "hardhat test",
14-
"verify:etherscan:arbitrum:sepolia": "dotenv -e .env -- bash -c 'npx @layerzerolabs/verify-contract verify-contract -d ./deployments -n arbitrum-testnet -u https://api.etherscan.io/v2/api?chainid=421614 -k $ETHERSCAN_API'",
14+
"verify:etherscan:bnb:mainnet": "dotenv -e .env -- bash -c 'npx @layerzerolabs/verify-contract verify-contract -d ./deployments -n bnb-mainnet -u https://api.etherscan.io/v2/api?chainid=56 -k $ETHERSCAN_API'",
15+
"verify:etherscan:bnb:testnet": "dotenv -e .env -- bash -c 'npx @layerzerolabs/verify-contract verify-contract -d ./deployments -n bnb-testnet -u https://api.etherscan.io/v2/api?chainid=97 -k $ETHERSCAN_API'",
1516
"verify:etherscan:ethereum:mainnet": "dotenv -e .env -- bash -c 'npx @layerzerolabs/verify-contract verify-contract -d ./deployments -n ethereum-mainnet -u https://api.etherscan.io/v2/api?chainid=1 -k $ETHERSCAN_API'",
1617
"verify:etherscan:ethereum:sepolia": "dotenv -e .env -- bash -c 'npx @layerzerolabs/verify-contract verify-contract -d ./deployments -n ethereum-testnet -u https://api.etherscan.io/v2/api?chainid=11155111 -k $ETHERSCAN_API'",
1718
"verify:etherscan:gateway:mainnet": "dotenv -e .env -- bash -c 'npx @layerzerolabs/verify-contract verify-contract -d ./deployments -n gateway-mainnet -u $BLOCKSCOUT_API'",
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
4+
interface InputItem {
5+
point: {
6+
eid: number;
7+
address: string;
8+
};
9+
data: string;
10+
description: string;
11+
}
12+
13+
interface AragonAction {
14+
to: string;
15+
value: number;
16+
data: string;
17+
}
18+
19+
function convertToAragonProposal(inputPath: string, outputPath: string): void {
20+
// Read input JSON
21+
const inputData: InputItem[] = JSON.parse(fs.readFileSync(inputPath, 'utf-8'));
22+
23+
// Transform to Aragon proposal format
24+
const aragonProposal: AragonAction[] = inputData.map((item) => ({
25+
to: item.point.address,
26+
value: 0,
27+
data: item.data,
28+
}));
29+
30+
// Write output JSON
31+
fs.writeFileSync(outputPath, JSON.stringify(aragonProposal, null, 2));
32+
33+
console.log(`✅ Converted ${inputData.length} actions to Aragon proposal format`);
34+
console.log(`📄 Output saved to: ${outputPath}`);
35+
}
36+
37+
// Main execution
38+
const inputFile = process.argv[2] || 'output.json';
39+
const outputFile = process.argv[3] || 'aragonProposal.json';
40+
41+
const inputPath = path.resolve(process.cwd(), inputFile);
42+
const outputPath = path.resolve(process.cwd(), outputFile);
43+
44+
if (!fs.existsSync(inputPath)) {
45+
console.error(`❌ Input file not found: ${inputPath}`);
46+
process.exit(1);
47+
}
48+
49+
convertToAragonProposal(inputPath, outputPath);

0 commit comments

Comments
 (0)