Merge branch 'tokenbridge' into mvp

This commit is contained in:
spacemandev 2022-06-29 23:56:43 -05:00
commit 052ea79f2a
23 changed files with 8642 additions and 0 deletions

3
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "projects/messenger/chains/evm/lib/forge-std"]
path = projects/messenger/chains/evm/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "projects/evm-tokenbridge/chains/evm/lib/openzeppelin-contracts"]
path = projects/evm-tokenbridge/chains/evm/lib/openzeppelin-contracts
url = https://github.com/openzeppelin/openzeppelin-contracts

3
projects/evm-tokenbridge/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules/
wormhole/
deployment.json

View File

@ -0,0 +1,22 @@
# EVM Messenger
Simple messenger project that sends a "Hello World" message between two EVM chains using Wormhole.
## Dependencies
This project uses Foundry to compile and deploy EVM contracts. You can find install instructions at [`https://getfoundry.sh`](http://getfoundry.sh)
The javascript dependencies can be installed via `npm install` in this folder.
You will also need Docker; you can get either [Docker Desktop](https://docs.docker.com/get-docker/) if you're developing on your computer or if you're in a headless vm, install [Docker Engine](https://docs.docker.com/engine/)
## Run Guardiand
After you have the dependencies installed, we'll need to spin up the EVM chains, deploy the Wormhole contracts to them, then startup a Wormhole Guardian to observe and sign VAAs. We have provided a script to automate this all for you.
Simply run `npm run guardiand` and wait while the Wormhole Guardian builds a docker image. The first time you run this command, it might take a while (up to 550 seconds on a modern laptop!). After the image is built however, it'll be relatively fast to bring it up and down.
## Test Scripts
After you have Guardiand running, you can run the basic test with `npm run test`. This will:
- Deploy a Treasury contract
- Attest the TKN ERC20 token from Chain0 (ETH) to Chain1 (BSC)
- Mint 100 TKN tokens to the Treasury on ETH
- Approve & Transfer 50 TKN tokens from the treasury on ETH to the Treasury on BSC.

View File

@ -0,0 +1,3 @@
cache/
out/
!src/Wormhole/

View File

@ -0,0 +1,7 @@
[default]
src = 'src'
out = 'out'
libs = ['lib']
solc_version = '0.8.10'
# See more config options https://github.com/foundry-rs/foundry/tree/master/config

@ -0,0 +1 @@
Subproject commit e734b42fc2245b520372bca0099870f40f1e6f38

View File

@ -0,0 +1 @@
@openzeppelin/=lib/openzeppelin-contracts

View File

@ -0,0 +1,52 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
//If the below line shows an error, ignore it, it's cause you're root folder is not chains/evm.
import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol";
import "./Wormhole/ITokenBridge.sol";
import "./Wormhole/PortalWrappedToken.sol";
contract Treasury {
address private token_bridge_address = address(0x0290FB167208Af455bB137780163b7B7a9a10C16);
ITokenBridge token_bridge = ITokenBridge(token_bridge_address);
address private TKN_address = address(0x2D8BE6BF0baA74e0A907016679CaE9190e80dD0A);
ERC20PresetMinterPauser TKN = ERC20PresetMinterPauser(TKN_address);
uint32 nonce = 0;
mapping(uint16 => bytes32) _applicationContracts;
mapping(bytes32 => bool) _completedMessages;
address owner;
constructor(){
owner = msg.sender;
}
/**
Registers it's sibling applications on other chains as the only ones that can send this instance messages
*/
function registerApplicationContracts(uint16 chainId, bytes32 applicationAddr) public {
require(msg.sender == owner, "Only owner can register new chains!");
_applicationContracts[chainId] = applicationAddr;
}
//Returns the Balance of this Contract
function getTKNCount() public view returns (uint256) {
return TKN.balanceOf(address(this));
}
//Returns the Balance of Wrapped Count
function getWrappedCount(PortalWrappedToken wrappedToken) public view returns (uint256) {
return wrappedToken.balanceOf(address(this));
}
function bridgeToken(uint256 amt, uint16 receipientChainId, bytes32 recipient) public returns (uint64 sequence) {
nonce += 1;
return token_bridge.transferTokens(TKN_address, amt, receipientChainId, recipient, 0, nonce);
}
function approveTokenBridge(uint256 amt) public returns (bool) {
return TKN.approve(token_bridge_address, amt);
}
}

View File

@ -0,0 +1,18 @@
// contracts/Getters.sol
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.0;
interface BridgeGetters {
function governanceActionIsConsumed(bytes32 hash) external view returns (bool) ;
function isInitialized(address impl) external view returns (bool) ;
function isTransferCompleted(bytes32 hash) external view returns (bool) ;
function chainId() external view returns (uint16);
function governanceChainId() external view returns (uint16);
function governanceContract() external view returns (bytes32);
function wrappedAsset(uint16 tokenChainId, bytes32 tokenAddress) external view returns (address);
function bridgeContracts(uint16 chainId_) external view returns (bytes32);
function tokenImplementation() external view returns (address);
function outstandingBridged(address token) external view returns (uint256);
function isWrappedAsset(address token) external view returns (bool);
}

View File

@ -0,0 +1,121 @@
// contracts/Bridge.sol
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.0;
import "./BridgeGetters.sol";
interface ITokenBridge is BridgeGetters {
/*
* @dev Produce a AssetMeta message for a given token
*/
function attestToken(address tokenAddress, uint32 nonce) external payable returns (uint64 sequence);
/*
* @notice Send eth through portal by first wrapping it to WETH.
*/
function wrapAndTransferETH(
uint16 recipientChain,
bytes32 recipient,
uint256 arbiterFee,
uint32 nonce
) external payable returns (uint64 sequence);
/*
* @notice Send eth through portal by first wrapping it.
*
* @dev This type of transfer is called a "contract-controlled transfer".
* There are three differences from a regular token transfer:
* 1) Additional arbitrary payload can be attached to the message
* 2) Only the recipient (typically a contract) can redeem the transaction
* 3) The sender's address (msg.sender) is also included in the transaction payload
*
* With these three additional components, xDapps can implement cross-chain
* composable interactions.
*/
function wrapAndTransferETHWithPayload(
uint16 recipientChain,
bytes32 recipient,
uint32 nonce,
bytes memory payload
) external payable returns (uint64 sequence);
/*
* @notice Send ERC20 token through portal.
*/
function transferTokens(
address token,
uint256 amount,
uint16 recipientChain,
bytes32 recipient,
uint256 arbiterFee,
uint32 nonce
) external payable returns (uint64 sequence);
/*
* @notice Send ERC20 token through portal.
*
* @dev This type of transfer is called a "contract-controlled transfer".
* There are three differences from a regular token transfer:
* 1) Additional arbitrary payload can be attached to the message
* 2) Only the recipient (typically a contract) can redeem the transaction
* 3) The sender's address (msg.sender) is also included in the transaction payload
*
* With these three additional components, xDapps can implement cross-chain
* composable interactions.
*/
function transferTokensWithPayload(
address token,
uint256 amount,
uint16 recipientChain,
bytes32 recipient,
uint32 nonce,
bytes memory payload
) external payable returns (uint64 sequence);
function updateWrapped(bytes memory encodedVm) external returns (address token);
function createWrapped(bytes memory encodedVm) external returns (address token);
/*
* @notice Complete a contract-controlled transfer of an ERC20 token.
*
* @dev The transaction can only be redeemed by the recipient, typically a
* contract.
*
* @param encodedVm A byte array containing a VAA signed by the guardians.
*
* @return The byte array representing a BridgeStructs.TransferWithPayload.
*/
function completeTransferWithPayload(bytes memory encodedVm) external returns (bytes memory);
/*
* @notice Complete a contract-controlled transfer of WETH, and unwrap to ETH.
*
* @dev The transaction can only be redeemed by the recipient, typically a
* contract.
*
* @param encodedVm A byte array containing a VAA signed by the guardians.
*
* @return The byte array representing a BridgeStructs.TransferWithPayload.
*/
function completeTransferAndUnwrapETHWithPayload(bytes memory encodedVm) external returns (bytes memory);
/*
* @notice Complete a transfer of an ERC20 token.
*
* @dev The msg.sender gets paid the associated fee.
*
* @param encodedVm A byte array containing a VAA signed by the guardians.
*/
function completeTransfer(bytes memory encodedVm) external ;
/*
* @notice Complete a transfer of WETH and unwrap to eth.
*
* @dev The msg.sender gets paid the associated fee.
*
* @param encodedVm A byte array containing a VAA signed by the guardians.
*/
function completeTransferAndUnwrapETH(bytes memory encodedVm) external ;
}

View File

@ -0,0 +1,42 @@
// contracts/TokenImplementation.sol
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.0;
// Based on the OpenZepplin ERC20 implementation, licensed under MIT
interface PortalWrappedToken {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function owner() external view returns (address);
function decimals() external view returns (uint8);
function totalSupply() external view returns (uint256);
function chainId() external view returns (uint16);
function nativeContract() external view returns (bytes32) ;
function balanceOf(address account_) external view returns (uint256) ;
function transfer(address recipient_, uint256 amount_) external returns (bool) ;
function allowance(address owner_, address spender_) external view returns (uint256) ;
function approve(address spender_, uint256 amount_) external returns (bool) ;
function transferFrom(address sender_, address recipient_, uint256 amount_) external returns (bool) ;
function increaseAllowance(address spender_, uint256 addedValue_) external returns (bool) ;
function decreaseAllowance(address spender_, uint256 subtractedValue_) external returns (bool) ;
function mint(address account_, uint256 amount_) external ;
}

View File

@ -0,0 +1,40 @@
// contracts/Structs.sol
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.0;
interface Structs {
struct Provider {
uint16 chainId;
uint16 governanceChainId;
bytes32 governanceContract;
}
struct GuardianSet {
address[] keys;
uint32 expirationTime;
}
struct Signature {
bytes32 r;
bytes32 s;
uint8 v;
uint8 guardianIndex;
}
struct VM {
uint8 version;
uint32 timestamp;
uint32 nonce;
uint16 emitterChainId;
bytes32 emitterAddress;
uint64 sequence;
uint8 consistencyLevel;
bytes payload;
uint32 guardianSetIndex;
Signature[] signatures;
bytes32 hash;
}
}

7659
projects/evm-tokenbridge/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
{
"name": "xdapp-starter",
"version": "1.0.0",
"description": "A simple template for getting started with xDapps.",
"main": "starter.js",
"scripts": {
"guardiand": "sh wormhole.sh",
"cleanup": "docker kill guardiand && docker rm guardiand && npx pm2 kill",
"test": "sh tests/treasury_bridge.sh"
},
"keywords": [],
"author": "",
"license": "MIT",
"workspaces": [],
"type": "module",
"dependencies": {
"@certusone/wormhole-sdk": "^0.3.3",
"byteify": "^2.0.10",
"ethers": "^5.6.9",
"ganache": "^7.3.1",
"node-fetch": "^3.2.6",
"pm2": "^5.2.0"
}
}

View File

@ -0,0 +1,11 @@
node treasury.js evm0 deploy
node treasury.js evm1 deploy
node treasury.js evm0 register_chain evm1
node treasury.js evm1 register_chain evm0
node treasury.js evm0 get_tokens 100
node treasury.js evm0 attest_token evm1
node treasury.js evm1 get_token_counts
node treasury.js evm0 bridge_token evm1 50
node treasury.js evm0 get_token_counts
node treasury.js evm1 get_token_counts

View File

@ -0,0 +1,341 @@
import { exec } from "child_process";
import fs from "fs";
import { BigNumber, ethers } from "ethers";
import {
getEmitterAddressEth,
parseSequenceFromLogEth,
attestFromEth,
tryNativeToHexString,
} from "@certusone/wormhole-sdk";
import fetch from "node-fetch";
async function main() {
let config = JSON.parse(fs.readFileSync("./xdapp.config.json").toString());
let network = config.networks[process.argv[2]];
let deployment;
try {
deployment = JSON.parse(fs.readFileSync("./deployment.json").toString());
} catch (e) {
deployment = {};
deployment[[process.argv[2]]] = {
deployedAddress: "",
emittedVAAs: []
};
}
if (!network) {
throw new Error("Network not defined in config file.");
}
if (process.argv[3] == "deploy") {
console.log(
`Deploying EVM network: ${process.argv[2]} to ${network.rpc}`
);
exec(
`cd chains/evm && forge install && forge build && forge create --legacy --rpc-url ${network.rpc} --private-key ${network.privateKey} src/Treasury.sol:Treasury && exit`,
(err, out, errStr) => {
if (err) {
throw new Error(err);
}
if (out) {
console.log(out);
deployment[[process.argv[2]]] = {
deployedAddress: "",
emittedVAAs: []
};
deployment[process.argv[2]].deployedAddress = out
.split("Deployed to: ")[1]
.split("\n")[0]
.trim();
deployment[process.argv[2]].emittedVAAs = [];
fs.writeFileSync(
"./deployment.json",
JSON.stringify(deployment, null, 4)
);
}
}
);
} else if (process.argv[3] == "register_chain") {
if (!deployment[process.argv[2]].deployedAddress) {
throw new Error("Deploy to this network first!");
}
const targetNetwork = config.networks[process.argv[4]];
const targetDeployment = deployment[process.argv[4]]
if (!targetDeployment.deployedAddress) {
throw new Error("Target Network not deployed yet!");
}
let emitterAddr = Buffer.from(
getEmitterAddressEth(targetDeployment.deployedAddress),
"hex"
);
const signer = new ethers.Wallet(network.privateKey).connect(
new ethers.providers.JsonRpcProvider(network.rpc)
);
const treasury = new ethers.Contract(
deployment[process.argv[2]].deployedAddress,
JSON.parse(
fs
.readFileSync(
"./chains/evm/out/Treasury.sol/Treasury.json"
)
.toString()
).abi,
signer
);
await treasury.registerApplicationContracts(
targetNetwork.wormholeChainId,
emitterAddr
);
console.log(
`Network(${process.argv[2]}) Registered Emitter: ${targetDeployment.deployedAddress} from Chain: ${process.argv[4]}`
);
} else if (process.argv[3] == "get_tokens") {
if (!deployment[process.argv[2]].deployedAddress) {
throw new Error("Deploy to this network first!");
}
const signer = new ethers.Wallet(network.privateKey).connect(
new ethers.providers.JsonRpcProvider(network.rpc)
);
const treasury = new ethers.Contract(
deployment[process.argv[2]].deployedAddress,
JSON.parse(
fs
.readFileSync(
"./chains/evm/out/Treasury.sol/Treasury.json"
)
.toString()
).abi,
signer
);
const TKN = new ethers.Contract(
network.testToken,
JSON.parse(
fs
.readFileSync(
"./chains/evm/out/ERC20PresetMinterPauser.sol/ERC20PresetMinterPauser.json"
)
.toString()
).abi,
signer
);
console.log(`${process.argv[2]} Treasury has ${await treasury.getTKNCount()} tokens.`);
const tokenAmt = ethers.utils.parseUnits(process.argv[4], "18");
console.log(`Minting ${tokenAmt} tokens.`);
await TKN.mint(deployment[process.argv[2]].deployedAddress, tokenAmt, {
gasLimit: 2000000
});
await new Promise((r) => setTimeout(r, 3000));
console.log(`${process.argv[2]} Treasury has ${await treasury.getTKNCount()} tokens.`);
} else if (process.argv[3] == "attest_token") {
if (!deployment[process.argv[2]].deployedAddress) {
throw new Error("Deploy to this network first!");
}
const signer = new ethers.Wallet(network.privateKey).connect(
new ethers.providers.JsonRpcProvider(network.rpc)
);
const networkTokenAttestation = await attestFromEth(
network.tokenBridgeAddress,
signer,
network.testToken
);
const emitterAddr = getEmitterAddressEth(network.tokenBridgeAddress);
const seq = parseSequenceFromLogEth(networkTokenAttestation, network.bridgeAddress);
const vaaURL = `${config.wormhole.restAddress}/v1/signed_vaa/${network.wormholeChainId}/${emitterAddr}/${seq}`;
console.log("Searching for: ", vaaURL);
let vaaBytes = await (await fetch(vaaURL)).json();
while(!vaaBytes.vaaBytes){
console.log("VAA not found, retrying in 5s!");
await new Promise((r) => setTimeout(r, 5000)); //Timeout to let Guardiand pick up log and have VAA ready
vaaBytes = await (await fetch(vaaURL)).json();
}
if (!deployment[process.argv[2]].emittedVAAs) {
deployment[process.argv[2]].emittedVAAs = [vaaBytes.vaaBytes];
} else {
deployment[process.argv[2]].emittedVAAs.push(vaaBytes.vaaBytes);
}
fs.writeFileSync(
"./deployment.json",
JSON.stringify(deployment, null, 2)
);
console.log(
`Network(${process.argv[2]}) Emitted VAA: `,
vaaBytes.vaaBytes
);
// Now create the Wrapped Version of the Token on the target chain
const targetNetwork = config.networks[process.argv[4]];
const targetDeployment = deployment[process.argv[4]];
const targetSigner = new ethers.Wallet(network.privateKey).connect(
new ethers.providers.JsonRpcProvider(targetNetwork.rpc)
);
const targetTokenBridge = new ethers.Contract(
targetNetwork.tokenBridgeAddress,
JSON.parse(
fs
.readFileSync(
"./chains/evm/out/ITokenBridge.sol/ITokenBridge.json"
)
.toString()
).abi,
targetSigner
);
await targetTokenBridge.createWrapped(Buffer.from(vaaBytes.vaaBytes, "base64"), {
gasLimit: 2000000
})
await new Promise((r) => setTimeout(r, 5000)); //Time out to let block propogate
const wrappedTokenAddress = await targetTokenBridge.wrappedAsset(network.wormholeChainId, Buffer.from(tryNativeToHexString(network.testToken, "ethereum"), "hex"));
console.log("Wrapped token created at: ", wrappedTokenAddress);
targetDeployment["wrappedTestTokenAddress"] = wrappedTokenAddress;
deployment[process.argv[4]] = targetDeployment;
fs.writeFileSync('./deployment.json', JSON.stringify(deployment, null, 4));
} else if (process.argv[3] == "get_token_counts") {
if (!deployment[process.argv[2]].deployedAddress) {
throw new Error("Deploy to this network first!");
}
const signer = new ethers.Wallet(network.privateKey).connect(
new ethers.providers.JsonRpcProvider(network.rpc)
);
const treasury = new ethers.Contract(
deployment[process.argv[2]].deployedAddress,
JSON.parse(
fs
.readFileSync(
"./chains/evm/out/Treasury.sol/Treasury.json"
)
.toString()
).abi,
signer
);
console.log(`${process.argv[2]} Treasury has ${await treasury.getTKNCount()} native TKN.`)
if(deployment[process.argv[2]]['wrappedTestTokenAddress']){
console.log(`${process.argv[2]} Treasury has ${await treasury.getWrappedCount(deployment[process.argv[2]]['wrappedTestTokenAddress'])} wrapped TKN.`)
}
} else if (process.argv[3] == "bridge_token") {
if (!deployment[process.argv[2]].deployedAddress) {
throw new Error("Deploy to this network first!");
}
const signer = new ethers.Wallet(network.privateKey).connect(
new ethers.providers.JsonRpcProvider(network.rpc)
);
const treasury = new ethers.Contract(
deployment[process.argv[2]].deployedAddress,
JSON.parse(
fs
.readFileSync(
"./chains/evm/out/Treasury.sol/Treasury.json"
)
.toString()
).abi,
signer
);
//Multiply out the decimals to 18 (default for ERC20)
const bridgeAmt = ethers.utils.parseUnits(process.argv[5], "18");
// Remember to allow Token Bridge to move tokens from Treasury account to it's own account
console.log(`Approving ${process.argv[5]} (${bridgeAmt} in proper ERC20 format) Tokens to be bridged by Token Bridge`);
await treasury.approveTokenBridge(bridgeAmt, {
gasLimit: 2000000,
});
await new Promise((r) => setTimeout(r, 5000)); //Time out to let block propogate
const targetNetwork = config.networks[process.argv[4]];
const targetDeployment = deployment[process.argv[4]]
if (!targetDeployment.deployedAddress) {
throw new Error("Target Network not deployed yet!");
}
console.log(`Bridging ${process.argv[5]} Tokens which is ${bridgeAmt} Tokens`);
const targetRecepient = Buffer.from(tryNativeToHexString(targetDeployment.deployedAddress, "ethereum"), 'hex');
const tx = await (await treasury.bridgeToken(bridgeAmt, targetNetwork.wormholeChainId, targetRecepient)).wait();
const emitterAddr = getEmitterAddressEth(network.tokenBridgeAddress);
const seq = parseSequenceFromLogEth(tx, network.bridgeAddress);
const vaaURL = `${config.wormhole.restAddress}/v1/signed_vaa/${network.wormholeChainId}/${emitterAddr}/${seq}`;
let vaaBytes = await (await fetch(vaaURL)).json();
while(!vaaBytes.vaaBytes){
console.log("VAA not found, retrying in 5s!");
await new Promise((r) => setTimeout(r, 5000)); //Timeout to let Guardiand pick up log and have VAA ready
vaaBytes = await (await fetch(vaaURL)).json();
}
if (!deployment[process.argv[2]].emittedVAAs) {
deployment[process.argv[2]].emittedVAAs = [vaaBytes.vaaBytes];
} else {
deployment[process.argv[2]].emittedVAAs.push(vaaBytes.vaaBytes);
}
fs.writeFileSync(
"./deployment.json",
JSON.stringify(deployment, null, 2)
);
console.log(
`Network(${process.argv[2]}) Emitted VAA: `,
vaaBytes.vaaBytes
);
// Now create the Wrapped Version of the Token on the target chain
const targetSigner = new ethers.Wallet(network.privateKey).connect(
new ethers.providers.JsonRpcProvider(targetNetwork.rpc)
);
const targetTokenBridge = new ethers.Contract(
targetNetwork.tokenBridgeAddress,
JSON.parse(
fs
.readFileSync(
"./chains/evm/out/ITokenBridge.sol/ITokenBridge.json"
)
.toString()
).abi,
targetSigner
);
const completeTransferTx = await targetTokenBridge.completeTransfer(Buffer.from(vaaBytes.vaaBytes, "base64"));
console.log("Complete Transfer TX: ", await completeTransferTx.wait());
} else if (process.argv[3] == "debug") {
const signer = new ethers.Wallet(network.privateKey).connect(
new ethers.providers.JsonRpcProvider(network.rpc)
);
const TKN = new ethers.Contract(
network.testToken,
JSON.parse(
fs
.readFileSync(
"./chains/evm/out/ERC20PresetMinterPauser.sol/ERC20PresetMinterPauser.json"
)
.toString()
).abi,
signer
);
console.log((await TKN.allowance(deployment[process.argv[2]].deployedAddress, network.tokenBridgeAddress)).toNumber());
console.log((await TKN.balanceOf(network.tokenBridgeAddress)).toNumber());
} else {
throw new Error("Unkown command!");
}
}
main();

View File

@ -0,0 +1,113 @@
#!/usr/bin/env bash
npm run cleanup
if [! docker info > /dev/null ] ; then
echo "This script uses docker, and it isn't running - please start docker and try again!"
exit 1
fi
# Check if wormhole/ repo exists.
# If it doens't then clone and build guardiand
if [ ! -d "./wormhole" ]
then
git clone https://github.com/certusone/wormhole
cd wormhole/
DOCKER_BUILDKIT=1 docker build --target go-export -f Dockerfile.proto -o type=local,dest=node .
DOCKER_BUILDKIT=1 docker build --target node-export -f Dockerfile.proto -o type=local,dest=. .
cd node/
echo "Have patience, this step takes upwards of 500 seconds!"
if [ $(uname -m) = "arm64" ]; then
echo "Building Guardian for linux/amd64"
DOCKER_BUILDKIT=1 docker build --platform linux/amd64 -f Dockerfile -t guardian .
else
echo "Building Guardian natively"
DOCKER_BUILDKIT=1 docker build -f Dockerfile -t guardian .
fi
cd ../../
fi
# Start EVM Chain 0
npx pm2 start 'ganache -p 8545 -m "myth like bonus scare over problem client lizard pioneer submit female collect" --block-time 1' --name evm0
# Start EVM Chain 1
npx pm2 start 'ganache -p 8546 -m "myth like bonus scare over problem client lizard pioneer submit female collect" --block-time 1' --name evm1
#Install Wormhole Eth Dependencies
cd wormhole/ethereum
npm i
cp .env.test .env
npm run build
# Deploy Wormhole Contracts to EVM Chain 0
npm run migrate && npx truffle exec scripts/deploy_test_token.js && npx truffle exec scripts/register_solana_chain.js && npx truffle exec scripts/register_terra_chain.js && npx truffle exec scripts/register_bsc_chain.js && npx truffle exec scripts/register_algo_chain.js
# Deploy Wormhole Contracts to EVM Chain 1
perl -pi -e 's/CHAIN_ID=0x2/CHAIN_ID=0x4/g' .env && perl -pi -e 's/8545/8546/g' truffle-config.js
npm run migrate && npx truffle exec scripts/deploy_test_token.js && npx truffle exec scripts/register_solana_chain.js && npx truffle exec scripts/register_terra_chain.js && npx truffle exec scripts/register_eth_chain.js && npx truffle exec scripts/register_algo_chain.js && nc -lkp 2000 0.0.0.0
perl -pi -e 's/CHAIN_ID=0x4/CHAIN_ID=0x2/g' .env && perl -pi -e 's/8546/8545/g' truffle-config.js
cd ../../
# Run Guardiand
if [ $(uname -m) = "arm64" ]; then
docker run -d --name guardiand -p 7070:7070 -p 7071:7071 -p 7073:7073 --platform linux/amd64 --hostname guardian-0 --cap-add=IPC_LOCK --entrypoint /guardiand guardian node \
--unsafeDevMode --guardianKey /tmp/bridge.key --publicRPC "[::]:7070" --publicWeb "[::]:7071" --adminSocket /tmp/admin.sock --dataDir /tmp/data \
--ethRPC ws://host.docker.internal:8545 \
--ethContract "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550" \
--bscRPC ws://host.docker.internal:8546 \
--bscContract "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550" \
--polygonRPC ws://host.docker.internal:8545 \
--avalancheRPC ws://host.docker.internal:8545 \
--auroraRPC ws://host.docker.internal:8545 \
--fantomRPC ws://host.docker.internal:8545 \
--oasisRPC ws://host.docker.internal:8545 \
--karuraRPC ws://host.docker.internal:8545 \
--acalaRPC ws://host.docker.internal:8545 \
--klaytnRPC ws://host.docker.internal:8545 \
--celoRPC ws://host.docker.internal:8545 \
--moonbeamRPC ws://host.docker.internal:8545 \
--neonRPC ws://host.docker.internal:8545 \
--terraWS ws://host.docker.internal:8545 \
--terra2WS ws://host.docker.internal:8545 \
--terraLCD https://host.docker.internal:1317 \
--terra2LCD http://host.docker.internal:1317 \
--terraContract terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5 \
--terra2Contract terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5 \
--solanaContract Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o \
--solanaWS ws://host.docker.internal:8900 \
--solanaRPC http://host.docker.internal:8899 \
--algorandIndexerRPC ws://host.docker.internal:8545 \
--algorandIndexerToken "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \
--algorandAlgodToken "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \
--algorandAlgodRPC https://host.docker.internal:4001 \
--algorandAppID "4"
else
docker run -d --name guardiand --network host --hostname guardian-0 --cap-add=IPC_LOCK --entrypoint /guardiand guardian node \
--unsafeDevMode --guardianKey /tmp/bridge.key --publicRPC "[::]:7070" --publicWeb "[::]:7071" --adminSocket /tmp/admin.sock --dataDir /tmp/data \
--ethRPC ws://localhost:8545 \
--ethContract "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550" \
--bscRPC ws://localhost:8546 \
--bscContract "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550" \
--polygonRPC ws://localhost:8545 \
--avalancheRPC ws://localhost:8545 \
--auroraRPC ws://localhost:8545 \
--fantomRPC ws://localhost:8545 \
--oasisRPC ws://localhost:8545 \
--karuraRPC ws://localhost:8545 \
--acalaRPC ws://localhost:8545 \
--klaytnRPC ws://localhost:8545 \
--celoRPC ws://localhost:8545 \
--moonbeamRPC ws://localhost:8545 \
--neonRPC ws://localhost:8545 \
--terraWS ws://localhost:8545 \
--terra2WS ws://localhost:8545 \
--terraLCD https://terra-terrad:1317 \
--terra2LCD http://localhost:1317 \
--terraContract terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5 \
--terra2Contract terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5 \
--solanaContract Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o \
--solanaWS ws://localhost:8900 \
--solanaRPC http://localhost:8899 \
--algorandIndexerRPC ws://localhost:8545 \
--algorandIndexerToken "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \
--algorandAlgodToken "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \
--algorandAlgodRPC https://localhost:4001 \
--algorandAppID "4"
fi
echo "Guardiand Running! To look at logs: \"docker logs guardiand -f\""

View File

@ -0,0 +1,25 @@
{
"networks": {
"evm0": {
"type": "evm",
"wormholeChainId": 2,
"rpc": "http://localhost:8545",
"privateKey": "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d",
"bridgeAddress": "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550",
"tokenBridgeAddress": "0x0290FB167208Af455bB137780163b7B7a9a10C16",
"testToken": "0x2D8BE6BF0baA74e0A907016679CaE9190e80dD0A"
},
"evm1": {
"type": "evm",
"wormholeChainId": 4,
"rpc": "http://localhost:8546",
"privateKey": "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d",
"tokenBridgeAddress": "0x0290FB167208Af455bB137780163b7B7a9a10C16",
"bridgeAddress": "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550",
"testToken": "0x2D8BE6BF0baA74e0A907016679CaE9190e80dD0A"
}
},
"wormhole": {
"restAddress": "http://localhost:7071"
}
}

View File

@ -0,0 +1,57 @@
# EVM: Attesting a Token
Attesting a token from EVM is fairly simple, and usually done via the Portal UI since it's a step that only needs to happen once per Token.
If for whatever reason you need to do it programmatically, you can also do it using the JS SDK:
The first step is to create an AttestMeta VAA. We do this by calling `attestFromEth()` function from the JS SDK and passing in the Token Bridge address, a Ethers signer object, and the address of the Token we want to attest:
```js
const networkTokenAttestation = await attestFromEth(
network.tokenBridgeAddress, // Token Bridge Address
signer, //Private Key to sign and pay for TX + RPC Endpoint
network.testToken //Token Address
);
```
Anyone can attest any token on the network.
To complete the Attestation, we grab the VAA that the `attestFromEth()` function generates by getting the Emitter address of the Token Bridge and the Sequence from the logs of the transaction receipt. We then fetch against a guardian REST endpoint. It could take a couple seconds (up to 30s!) for the guardian to see and sign the VAA, so it's a good idea to poll the guardian every couple seconds until the VAA is found.
```js
const emitterAddr = getEmitterAddressEth(network.tokenBridgeAddress);
const seq = parseSequenceFromLogEth(networkTokenAttestation, network.bridgeAddress);
const vaaURL = `${config.wormhole.restAddress}/v1/signed_vaa/${network.wormholeChainId}/${emitterAddr}/${seq}`;
console.log("Searching for: ", vaaURL);
let vaaBytes = await (await fetch(vaaURL)).json();
while(!vaaBytes.vaaBytes){
console.log("VAA not found, retrying in 5s!");
await new Promise((r) => setTimeout(r, 5000)); //Timeout to let Guardiand pick up log and have VAA ready
vaaBytes = await (await fetch(vaaURL)).json();
}
```
Next, we submit the VAA onto the target chain to create a wrapped version of the Token by calling `createWrapped()`. On an EVM chain, this will deploy a Portal Wrapped Token contract who's mint authority is the Portal Token Bridge on that chain. Sometimes this transaction throws an unpredicatable gas price error, so it's a good idea to set a high gas limit.
After the wrapped token is created, you can get the new wrapped token address by calling the `wrappedAsset()` function of the TokenBridge.
```js
await targetTokenBridge.createWrapped(Buffer.from(vaaBytes.vaaBytes, "base64"), {
gasLimit: 2000000
});
await new Promise((r) => setTimeout(r, 5000)); //Time out to let block propogate
const wrappedTokenAddress = await targetTokenBridge.wrappedAsset(
network.wormholeChainId,
Buffer.from(
tryNativeToHexString(network.testToken, "ethereum"),
"hex"
)
);
console.log("Wrapped token created at: ", wrappedTokenAddress);
```

View File

@ -0,0 +1,80 @@
# EVM: Transferring a Token
WARNING: To be able to successfully transfer a token from Chain A to Chain B, make sure you [attest](./attestingToken.md) it first. Otherwise the Token may be transferred, but you won't be able to claim it til it's attested.
One big gotcha new EVM developers usually run into when working with ERC20 tokens is that because EVM uses unsigned integers, there's no concept of decimals. Therefore, tokens usually have up to 18 zeros behind them to denote up to 18 decimal places. Wormhole normalizes this to *eight* zeros, with transfer amounts rounded down to the nearest 8the decimal.
To wrap the Token Bridge functions in your contract, you can use the Token Bridge interfaces provided under [`projects/evm-tokenbridge/chains/evm/src/Wormhole`](https://github.com/certusone/xdapp-book/tree/main/projects/evm-tokenbridge/chains/evm/src/Wormhole) folder of the xDapp Book repository.
```solidity
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol";
import "./Wormhole/ITokenBridge.sol";
import "./Wormhole/PortalWrappedToken.sol";
contract Treasury {
address private token_bridge_address = address(0x0290FB167208Af455bB137780163b7B7a9a10C16);
ITokenBridge token_bridge = ITokenBridge(token_bridge_address);
address private TKN_address = address(0x2D8BE6BF0baA74e0A907016679CaE9190e80dD0A);
ERC20PresetMinterPauser TKN = ERC20PresetMinterPauser(TKN_address);
uint32 nonce = 0;
function bridgeToken(uint256 amt, uint16 receipientChainId, bytes32 recipient) public returns (uint64 sequence) {
nonce += 1;
return token_bridge.transferTokens(TKN_address, amt, receipientChainId, recipient, 0, nonce);
}
function approveTokenBridge(uint256 amt) public returns (bool) {
return TKN.approve(token_bridge_address, amt);
}
}
```
To transfer a token, first we have to *approve* the Token Bridge to be able to spend that Token on our behalf (so it can transfer tokens form our contract to tself). Make sure the `bridgeAmt` properly takes into account decimals for the ERC20 token.
```js
// Here we are approving and transfering 50 tokens. The ERC20 token we are transfering has 18 decimal places.
const bridgeAmt = ethers.utils.parseUnits("50", "18");
await treasury.approveTokenBridge(bridgeAmt, {
gasLimit: 2000000,
});
```
Then we simply call `transfer` to create the transfer VAA and fetch it from the guardians when it's ready. Note that the target receipient is a Wormhole normalized hex address left-padded to 32 bytes.
```js
const targetRecepient = Buffer.from(tryNativeToHexString(targetDeployment.deployedAddress, "ethereum"), 'hex');
const tx = await (await treasury.bridgeToken(
bridgeAmt,
targetNetwork.wormholeChainId,
targetRecepient
)).wait();
const emitterAddr = getEmitterAddressEth(network.tokenBridgeAddress);
const seq = parseSequenceFromLogEth(tx, network.bridgeAddress);
const vaaURL = `${config.wormhole.restAddress}/v1/signed_vaa/${network.wormholeChainId}/${emitterAddr}/${seq}`;
let vaaBytes = await (await fetch(vaaURL)).json();
while(!vaaBytes.vaaBytes){
console.log("VAA not found, retrying in 5s!");
await new Promise((r) => setTimeout(r, 5000)); //Timeout to let Guardiand pick up log and have VAA ready
vaaBytes = await (await fetch(vaaURL)).json();
}
```
After we've fetched the VAA, we can call the `completeTransfer()` function on the target chain if it's an EVM.
```js
const completeTransferTx = await targetTokenBridge.completeTransfer(Buffer.from(vaaBytes.vaaBytes, "base64"));
```

View File

@ -0,0 +1 @@
# EVM: Transfer with Payload

View File

@ -0,0 +1,18 @@
# Portal Token Bridge Transfers
One of the biggest applications built on Wormhole is the Portal Token Bridge, which uses structured payloads to transfer tokens and NFTs from one wallet to another.
## Attesting a Token
Before a token can be transferred, the token need to be *attested* to another chain. To attest a token, you first create a AssetMeta VAA by calling the `attest()` function on Token Bridge. Then you take the VAA over to the receipient chain, where you call `createWrapped()` which deploys a wrapped version of the Token.
This only needs to happen *once* per payload, and trying to attest a token a second time will simply result in the address of the already created Wrapped Token Address.
## Transfering Tokens
To transfer tokens, the payer of tokens first authorizes the Token Bridge contract to move the tokens on their behalf, then locks them up with the Token Bridge, which emits a VAA. This VAA can then be submitted on target chain's Token Bridge's `completeTransfer()` to mint the wrapped version of the Token.
If the token being transferred is native to the chain it's being transfered to, you'll receive the original token back, rather than a wrapped version.
When transfering tokens from Chain A to B to C, the token is only "wrapped" once, as every time it's attested, it's always from the chain the token is natively located on.
Usually there's two functions for transfer, `transfer()` and `transferNative()`. This is because native currencies of most blockchains (ETH on Ethereum, SOL on Solana, etc) don't follow the Token spec of that chain, so to transfer native currencies, we usually wrap them first into a tokenized version of them and then transfer.
For transfers, there's also an arbiterFee you can set. If this fee is set, when `completeTransfer()` is called, that amount of tokens are withheld from the release of tokens on the target chain and instead given to the submitter of the message (for example, a relayer). This allows third party to submit transactions on your behalf, for a fee.
## Transfering with a Payload
Transfering with a Payload is much like transfering normal tokens, with two major differences. First, as the name implies, you can attach a bytes payload to the transfer message. Secondly, the `completeTransfer()` function for Transfer with Payload can *only* be called by the receipient of that VAA. This means the flow is slightly different, instead of the user calling the `completeTransfer()` function on the Token Bridge, they call a function on the application they are interacting with, which will check the payload, do what state changes it needs to make and then call `completeTransfer()` on Token Bridge to mint tokens to itself.