Merge branch 'mvp'
This commit is contained in:
commit
5a0b9b9075
|
@ -4,3 +4,12 @@
|
||||||
[submodule "projects/evm-tokenbridge/chains/evm/lib/openzeppelin-contracts"]
|
[submodule "projects/evm-tokenbridge/chains/evm/lib/openzeppelin-contracts"]
|
||||||
path = projects/evm-tokenbridge/chains/evm/lib/openzeppelin-contracts
|
path = projects/evm-tokenbridge/chains/evm/lib/openzeppelin-contracts
|
||||||
url = https://github.com/openzeppelin/openzeppelin-contracts
|
url = https://github.com/openzeppelin/openzeppelin-contracts
|
||||||
|
[submodule "projects/xmint/chains/evm/lib/openzeppelin-contracts"]
|
||||||
|
path = projects/xmint/chains/evm/lib/openzeppelin-contracts
|
||||||
|
url = https://github.com/OpenZeppelin/openzeppelin-contracts
|
||||||
|
[submodule "projects/xmint/chains/evm/lib/solidity-bytes-utils"]
|
||||||
|
path = projects/xmint/chains/evm/lib/solidity-bytes-utils
|
||||||
|
url = https://github.com/GNSPS/solidity-bytes-utils
|
||||||
|
[submodule "projects/xmint/chains/evm/lib/forge-std"]
|
||||||
|
path = projects/xmint/chains/evm/lib/forge-std
|
||||||
|
url = https://github.com/foundry-rs/forge-std
|
||||||
|
|
|
@ -16,9 +16,9 @@ cd wormhole/ethereum
|
||||||
make .env build
|
make .env build
|
||||||
|
|
||||||
# Deploy Wormhole Contracts to EVM Chain 0
|
# 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_eth_chain.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_bsc_chain.js
|
||||||
|
|
||||||
# Deploy Wormhole Contracts to EVM Chain 1
|
# 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
|
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
|
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_bsc_chain.js
|
||||||
perl -pi -e 's/CHAIN_ID=0x4/CHAIN_ID=0x2/g' .env && perl -pi -e 's/8546/8545/g' truffle-config.js
|
perl -pi -e 's/CHAIN_ID=0x4/CHAIN_ID=0x2/g' .env && perl -pi -e 's/8546/8545/g' truffle-config.js
|
||||||
|
|
|
@ -2,7 +2,10 @@
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Start guardiand
|
# Check if Docker is running
|
||||||
|
docker ps > /dev/null #route error to console
|
||||||
|
echo "Docker is running"
|
||||||
|
|
||||||
|
# Start guardiand
|
||||||
npx pm2 delete guardiand 2> /dev/null || true
|
npx pm2 delete guardiand 2> /dev/null || true
|
||||||
npx pm2 start 'bash guardiand.bash' --name guardiand
|
npx pm2 start 'bash guardiand.bash' --name guardiand
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
deployinfo/*.deploy.json
|
||||||
|
keypairs/*.key
|
||||||
|
node_modules/
|
|
@ -0,0 +1,2 @@
|
||||||
|
out/
|
||||||
|
cache/
|
|
@ -0,0 +1,7 @@
|
||||||
|
[default]
|
||||||
|
solc_version = "0.8.13"
|
||||||
|
src = 'src'
|
||||||
|
out = 'out'
|
||||||
|
libs = ['lib']
|
||||||
|
|
||||||
|
# See more config options https://github.com/foundry-rs/foundry/tree/master/config
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 2c7cbfc6fbede6d7c9e6b17afe997e3fdfe22fef
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 2dc086563f2e51620ebc43d2237fc10ef201c4e6
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 6458fb2780a3092bc756e737f246be1de6d3d362
|
|
@ -0,0 +1,2 @@
|
||||||
|
@openzeppelin=lib/openzeppelin-contracts
|
||||||
|
solidity-bytes-utils=lib/solidity-bytes-utils/contracts
|
|
@ -0,0 +1,12 @@
|
||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity ^0.8.13;
|
||||||
|
|
||||||
|
import "forge-std/Script.sol";
|
||||||
|
|
||||||
|
contract ContractScript is Script {
|
||||||
|
function setUp() public {}
|
||||||
|
|
||||||
|
function run() public {
|
||||||
|
vm.broadcast();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
// contracts/Structs.sol
|
||||||
|
// SPDX-License-Identifier: Apache 2
|
||||||
|
|
||||||
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
|
contract BridgeStructs {
|
||||||
|
struct Transfer {
|
||||||
|
// PayloadID uint8 = 1
|
||||||
|
uint8 payloadID;
|
||||||
|
// Amount being transferred (big-endian uint256)
|
||||||
|
uint256 amount;
|
||||||
|
// Address of the token. Left-zero-padded if shorter than 32 bytes
|
||||||
|
bytes32 tokenAddress;
|
||||||
|
// Chain ID of the token
|
||||||
|
uint16 tokenChain;
|
||||||
|
// Address of the recipient. Left-zero-padded if shorter than 32 bytes
|
||||||
|
bytes32 to;
|
||||||
|
// Chain ID of the recipient
|
||||||
|
uint16 toChain;
|
||||||
|
// Amount of tokens (big-endian uint256) that the user is willing to pay as relayer fee. Must be <= Amount.
|
||||||
|
uint256 fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TransferWithPayload {
|
||||||
|
// PayloadID uint8 = 3
|
||||||
|
uint8 payloadID;
|
||||||
|
// Amount being transferred (big-endian uint256)
|
||||||
|
uint256 amount;
|
||||||
|
// Address of the token. Left-zero-padded if shorter than 32 bytes
|
||||||
|
bytes32 tokenAddress;
|
||||||
|
// Chain ID of the token
|
||||||
|
uint16 tokenChain;
|
||||||
|
// Address of the recipient. Left-zero-padded if shorter than 32 bytes
|
||||||
|
bytes32 to;
|
||||||
|
// Chain ID of the recipient
|
||||||
|
uint16 toChain;
|
||||||
|
// Address of the message sender. Left-zero-padded if shorter than 32 bytes
|
||||||
|
bytes32 fromAddress;
|
||||||
|
// An arbitrary payload
|
||||||
|
bytes payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TransferResult {
|
||||||
|
// Chain ID of the token
|
||||||
|
uint16 tokenChain;
|
||||||
|
// Address of the token. Left-zero-padded if shorter than 32 bytes
|
||||||
|
bytes32 tokenAddress;
|
||||||
|
// Amount being transferred (big-endian uint256)
|
||||||
|
uint256 normalizedAmount;
|
||||||
|
// Amount of tokens (big-endian uint256) that the user is willing to pay as relayer fee. Must be <= Amount.
|
||||||
|
uint256 normalizedArbiterFee;
|
||||||
|
// Portion of msg.value to be paid as the core bridge fee
|
||||||
|
uint wormholeFee;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AssetMeta {
|
||||||
|
// PayloadID uint8 = 2
|
||||||
|
uint8 payloadID;
|
||||||
|
// Address of the token. Left-zero-padded if shorter than 32 bytes
|
||||||
|
bytes32 tokenAddress;
|
||||||
|
// Chain ID of the token
|
||||||
|
uint16 tokenChain;
|
||||||
|
// Number of decimals of the token (big-endian uint256)
|
||||||
|
uint8 decimals;
|
||||||
|
// Symbol of the token (UTF-8)
|
||||||
|
bytes32 symbol;
|
||||||
|
// Name of the token (UTF-8)
|
||||||
|
bytes32 name;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RegisterChain {
|
||||||
|
// Governance Header
|
||||||
|
// module: "TokenBridge" left-padded
|
||||||
|
bytes32 module;
|
||||||
|
// governance action: 1
|
||||||
|
uint8 action;
|
||||||
|
// governance paket chain id: this or 0
|
||||||
|
uint16 chainId;
|
||||||
|
|
||||||
|
// Chain ID
|
||||||
|
uint16 emitterChainID;
|
||||||
|
// Emitter address. Left-zero-padded if shorter than 32 bytes
|
||||||
|
bytes32 emitterAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UpgradeContract {
|
||||||
|
// Governance Header
|
||||||
|
// module: "TokenBridge" left-padded
|
||||||
|
bytes32 module;
|
||||||
|
// governance action: 2
|
||||||
|
uint8 action;
|
||||||
|
// governance paket chain id
|
||||||
|
uint16 chainId;
|
||||||
|
|
||||||
|
// Address of the new contract
|
||||||
|
bytes32 newContract;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 ;
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
// contracts/Messages.sol
|
||||||
|
// SPDX-License-Identifier: Apache 2
|
||||||
|
|
||||||
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
|
import "./Structs.sol";
|
||||||
|
|
||||||
|
interface IWormhole is Structs {
|
||||||
|
event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel);
|
||||||
|
|
||||||
|
function publishMessage(
|
||||||
|
uint32 nonce,
|
||||||
|
bytes memory payload,
|
||||||
|
uint8 consistencyLevel
|
||||||
|
) external payable returns (uint64 sequence);
|
||||||
|
|
||||||
|
function parseAndVerifyVM(bytes calldata encodedVM) external view returns (Structs.VM memory vm, bool valid, string memory reason);
|
||||||
|
|
||||||
|
function verifyVM(Structs.VM memory vm) external view returns (bool valid, string memory reason);
|
||||||
|
|
||||||
|
function verifySignatures(bytes32 hash, Structs.Signature[] memory signatures, Structs.GuardianSet memory guardianSet) external pure returns (bool valid, string memory reason) ;
|
||||||
|
|
||||||
|
function parseVM(bytes memory encodedVM) external pure returns (Structs.VM memory vm);
|
||||||
|
|
||||||
|
function getGuardianSet(uint32 index) external view returns (Structs.GuardianSet memory) ;
|
||||||
|
|
||||||
|
function getCurrentGuardianSetIndex() external view returns (uint32) ;
|
||||||
|
|
||||||
|
function getGuardianSetExpiry() external view returns (uint32) ;
|
||||||
|
|
||||||
|
function governanceActionIsConsumed(bytes32 hash) external view returns (bool) ;
|
||||||
|
|
||||||
|
function isInitialized(address impl) external view returns (bool) ;
|
||||||
|
|
||||||
|
function chainId() external view returns (uint16) ;
|
||||||
|
|
||||||
|
function governanceChainId() external view returns (uint16);
|
||||||
|
|
||||||
|
function governanceContract() external view returns (bytes32);
|
||||||
|
|
||||||
|
function messageFee() external view returns (uint256) ;
|
||||||
|
}
|
|
@ -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 ;
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity ^0.8.13;
|
||||||
|
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
|
import "./Wormhole/IWormhole.sol";
|
||||||
|
import "./Wormhole/ITokenBridge.sol";
|
||||||
|
import "./Wormhole/BridgeStructs.sol";
|
||||||
|
import "solidity-bytes-utils/BytesLib.sol";
|
||||||
|
|
||||||
|
contract Xmint is ERC20 {
|
||||||
|
using BytesLib for bytes;
|
||||||
|
|
||||||
|
mapping(uint16 => bytes32) _applicationContracts;
|
||||||
|
mapping(bytes32 => bool) _completedMessages;
|
||||||
|
address owner;
|
||||||
|
IWormhole core_bridge;
|
||||||
|
ITokenBridge token_bridge;
|
||||||
|
uint32 nonce = 0;
|
||||||
|
event Log(string indexed str);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
string memory name_,
|
||||||
|
string memory symbol_,
|
||||||
|
address coreBridgeAddress,
|
||||||
|
address tokenBridgeAddress
|
||||||
|
) ERC20(name_, symbol_) {
|
||||||
|
owner = msg.sender;
|
||||||
|
core_bridge = IWormhole(coreBridgeAddress);
|
||||||
|
token_bridge = ITokenBridge(tokenBridgeAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Takes inventory of the foreign currency
|
||||||
|
Mints tokens to self
|
||||||
|
Transfers tokens with Payload 1 to Receipient on Foreign chain
|
||||||
|
*/
|
||||||
|
function submitForeignPurchase(bytes memory encodedVm) public returns (uint64) {
|
||||||
|
// Complete transfer will give the Tokens to this Contract
|
||||||
|
BridgeStructs.TransferWithPayload memory vaa = _decodePayload(token_bridge.completeTransferWithPayload(encodedVm));
|
||||||
|
// Mint tokens to this contract
|
||||||
|
//amt they paid is NATIVE
|
||||||
|
//multiply by 100 to get how many tokens to give out
|
||||||
|
uint256 amtToMint = vaa.amount * 100;
|
||||||
|
_mint(address(this), amtToMint);
|
||||||
|
// Give Token Bridge allowance to take tokens from this contract
|
||||||
|
this.approve(address(token_bridge), amtToMint);
|
||||||
|
// Transfer tokens via Token Bridge over to Recipient in payload
|
||||||
|
uint64 sequence = token_bridge.transferTokens(address(this), amtToMint, vaa.tokenChain, bytes32(vaa.payload), 0, nonce);
|
||||||
|
nonce += 1;
|
||||||
|
return sequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _decodePayload(bytes memory payload) internal pure returns (BridgeStructs.TransferWithPayload memory) {
|
||||||
|
BridgeStructs.TransferWithPayload memory decoded = BridgeStructs.TransferWithPayload({
|
||||||
|
payloadID: payload.slice(0,1).toUint8(0),
|
||||||
|
amount: payload.slice(1,32).toUint256(0),
|
||||||
|
tokenAddress: payload.slice(33,32).toBytes32(0),
|
||||||
|
tokenChain: payload.slice(65,2).toUint16(0),
|
||||||
|
to: payload.slice(67,32).toBytes32(0),
|
||||||
|
toChain: payload.slice(99,2).toUint16(0),
|
||||||
|
fromAddress: payload.slice(101,32).toBytes32(0),
|
||||||
|
payload: payload.slice(133, payload.length-133)
|
||||||
|
});
|
||||||
|
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity ^0.8.13;
|
||||||
|
|
||||||
|
import "forge-std/Test.sol";
|
||||||
|
|
||||||
|
contract ContractTest is Test {
|
||||||
|
function setUp() public {}
|
||||||
|
|
||||||
|
function testExample() public {
|
||||||
|
assertTrue(true);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
.anchor
|
||||||
|
.DS_Store
|
||||||
|
target
|
||||||
|
**/*.rs.bk
|
||||||
|
node_modules
|
||||||
|
test-ledger
|
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
.anchor
|
||||||
|
.DS_Store
|
||||||
|
target
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
test-ledger
|
|
@ -0,0 +1,15 @@
|
||||||
|
[features]
|
||||||
|
seeds = false
|
||||||
|
skip-lint = false
|
||||||
|
[programs.localnet]
|
||||||
|
solana = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
|
||||||
|
|
||||||
|
[registry]
|
||||||
|
url = "https://api.apr.dev"
|
||||||
|
|
||||||
|
[provider]
|
||||||
|
cluster = "localnet"
|
||||||
|
wallet = "/Users/spacemandev/.config/solana/id.json"
|
||||||
|
|
||||||
|
[scripts]
|
||||||
|
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
|
|
@ -0,0 +1,13 @@
|
||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"programs/*"
|
||||||
|
]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
overflow-checks = true
|
||||||
|
lto = "fat"
|
||||||
|
codegen-units = 1
|
||||||
|
[profile.release.build-override]
|
||||||
|
opt-level = 3
|
||||||
|
incremental = false
|
||||||
|
codegen-units = 1
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Migrations are an early feature. Currently, they're nothing more than this
|
||||||
|
// single deploy script that's invoked from the CLI, injecting a provider
|
||||||
|
// configured from the workspace's Anchor.toml.
|
||||||
|
|
||||||
|
const anchor = require("@project-serum/anchor");
|
||||||
|
|
||||||
|
module.exports = async function (provider) {
|
||||||
|
// Configure client to use the provider.
|
||||||
|
anchor.setProvider(provider);
|
||||||
|
|
||||||
|
// Add your deploy script here.
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
|
||||||
|
"lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@project-serum/anchor": "^0.25.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"chai": "^4.3.4",
|
||||||
|
"mocha": "^9.0.3",
|
||||||
|
"ts-mocha": "^10.0.0",
|
||||||
|
"@types/bn.js": "^5.1.0",
|
||||||
|
"@types/chai": "^4.3.0",
|
||||||
|
"@types/mocha": "^9.0.0",
|
||||||
|
"typescript": "^4.3.5",
|
||||||
|
"prettier": "^2.6.2"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "solana"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Created with Anchor"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "lib"]
|
||||||
|
name = "solana"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
no-entrypoint = []
|
||||||
|
no-idl = []
|
||||||
|
no-log-ix-name = []
|
||||||
|
cpi = ["no-entrypoint"]
|
||||||
|
default = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anchor-lang = "0.25.0"
|
|
@ -0,0 +1,2 @@
|
||||||
|
[target.bpfel-unknown-unknown.dependencies.std]
|
||||||
|
features = []
|
|
@ -0,0 +1,15 @@
|
||||||
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
|
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
|
||||||
|
|
||||||
|
#[program]
|
||||||
|
pub mod solana {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct Initialize {}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import * as anchor from "@project-serum/anchor";
|
||||||
|
import { Program } from "@project-serum/anchor";
|
||||||
|
import { Solana } from "../target/types/solana";
|
||||||
|
|
||||||
|
describe("solana", () => {
|
||||||
|
// Configure the client to use the local cluster.
|
||||||
|
anchor.setProvider(anchor.AnchorProvider.env());
|
||||||
|
|
||||||
|
const program = anchor.workspace.Solana as Program<Solana>;
|
||||||
|
|
||||||
|
it("Is initialized!", async () => {
|
||||||
|
// Add your test here.
|
||||||
|
const tx = await program.methods.initialize().rpc();
|
||||||
|
console.log("Your transaction signature", tx);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["mocha", "chai"],
|
||||||
|
"typeRoots": ["./node_modules/@types"],
|
||||||
|
"lib": ["es2015"],
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es6",
|
||||||
|
"esModuleInterop": true
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,451 @@
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import {
|
||||||
|
attestFromEth,
|
||||||
|
createWrappedOnEth,
|
||||||
|
getEmitterAddressEth,
|
||||||
|
getEmitterAddressSolana,
|
||||||
|
getForeignAssetEth,
|
||||||
|
parseSequenceFromLogEth,
|
||||||
|
redeemOnEth,
|
||||||
|
setDefaultWasm,
|
||||||
|
transferFromEthNative,
|
||||||
|
tryNativeToUint8Array,
|
||||||
|
} from '@certusone/wormhole-sdk';
|
||||||
|
import * as ethers from 'ethers';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import * as solana from './solana';
|
||||||
|
|
||||||
|
const exec = promisify(require('child_process').exec);
|
||||||
|
const config = JSON.parse(fs.readFileSync('./xdapp.config.json').toString());
|
||||||
|
|
||||||
|
let ABI;
|
||||||
|
try {
|
||||||
|
ABI = JSON.parse(fs.readFileSync("./chains/evm/out/Xmint.sol/Xmint.json").toString()).abi
|
||||||
|
} catch (e) {
|
||||||
|
// fail silenty
|
||||||
|
// The only time this fails is when deploy hasn't been called, in which case, this isn't needed
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Deploy on chain contract "XMint"
|
||||||
|
* @param src The network to deploy
|
||||||
|
*/
|
||||||
|
export async function deploy(src: string){
|
||||||
|
const rpc = config.networks[src]['rpc'];
|
||||||
|
const core = config.networks[src]['bridgeAddress'];
|
||||||
|
const token = config.networks[src]['tokenBridgeAddress'];
|
||||||
|
const key = fs.readFileSync(`keypairs/${src}.key`).toString();
|
||||||
|
const { stdout , stderr } = await exec(
|
||||||
|
`cd chains/evm && forge build && forge create --legacy --rpc-url ${rpc} --private-key ${key} src/Xmint.sol:Xmint --constructor-args "${src.toUpperCase()}-TOKEN" "${src.toUpperCase()}T" ${core} ${token} && exit`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
throw new Error(stderr.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
let deploymentAddress:string;
|
||||||
|
if (stdout) {
|
||||||
|
console.log(stdout);
|
||||||
|
deploymentAddress = stdout
|
||||||
|
.split("Deployed to: ")[1]
|
||||||
|
.split("\n")[0]
|
||||||
|
.trim();
|
||||||
|
const emittedVAAs = []; //Resets the emittedVAAs
|
||||||
|
fs.writeFileSync(
|
||||||
|
`./deployinfo/${src}.deploy.json`,
|
||||||
|
JSON.stringify({
|
||||||
|
address: deploymentAddress,
|
||||||
|
vaas: emittedVAAs
|
||||||
|
}, null, 4)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the cross chain mint contracts with one another across chains
|
||||||
|
* @param src The network you want to register the foreign network on.
|
||||||
|
* @param target The foreign network
|
||||||
|
*/
|
||||||
|
export async function registerApp(src:string, target:string){
|
||||||
|
const key = fs.readFileSync(`keypairs/${src}.key`).toString();
|
||||||
|
|
||||||
|
const srcNetwork = config.networks[src];
|
||||||
|
const targetNetwork = config.networks[target];
|
||||||
|
let srcDeploymentInfo;
|
||||||
|
let targetDeploymentInfo;
|
||||||
|
let targetEmitter;
|
||||||
|
|
||||||
|
try{
|
||||||
|
srcDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${src}.deploy.json`).toString());
|
||||||
|
} catch (e){
|
||||||
|
throw new Error(`${src} is not deployed yet`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try{
|
||||||
|
targetDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${target}.deploy.json`).toString());
|
||||||
|
} catch (e){
|
||||||
|
throw new Error(`${target} is not deployed yet`);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (targetNetwork['type']){
|
||||||
|
case 'evm':
|
||||||
|
targetEmitter = getEmitterAddressEth(targetDeploymentInfo['address']);
|
||||||
|
break;
|
||||||
|
case 'solana':
|
||||||
|
setDefaultWasm("node"); // *sigh*
|
||||||
|
targetEmitter = await getEmitterAddressSolana(targetDeploymentInfo['address']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emitterBuffer = Buffer.from(targetEmitter, 'hex');
|
||||||
|
const signer = new ethers.Wallet(key).connect(
|
||||||
|
new ethers.providers.JsonRpcProvider(srcNetwork.rpc)
|
||||||
|
);
|
||||||
|
|
||||||
|
const messenger = new ethers.Contract(
|
||||||
|
srcDeploymentInfo.address,
|
||||||
|
ABI,
|
||||||
|
signer
|
||||||
|
);
|
||||||
|
|
||||||
|
const tx = await messenger.registerApplicationContracts(
|
||||||
|
targetNetwork.wormholeChainId,
|
||||||
|
emitterBuffer
|
||||||
|
);
|
||||||
|
console.log(`Registered ${target} application on ${src}`);
|
||||||
|
|
||||||
|
// Alongside registering the App, go ahead register the tokens with one another
|
||||||
|
// Register target token with src chain
|
||||||
|
console.log(`Registering ${target} token on ${src}`);
|
||||||
|
switch(targetNetwork.type){
|
||||||
|
case 'evm':
|
||||||
|
await attest(target, src);
|
||||||
|
break;
|
||||||
|
case 'solana':
|
||||||
|
await solana.attest(target, src);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
console.log(`Attested ${target} token on ${src}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attest token from src and create wrapped on target
|
||||||
|
* @param src
|
||||||
|
* @param target
|
||||||
|
* @param address
|
||||||
|
*/
|
||||||
|
export async function attest(src: string, target: string, address:string = null){
|
||||||
|
//Check TARGET type == EVM, else throw error
|
||||||
|
const srcNetwork = config.networks[src];
|
||||||
|
const targetNetwork = config.networks[target];
|
||||||
|
const srcDeployInfo = JSON.parse(fs.readFileSync(`./deployinfo/${src}.deploy.json`).toString());
|
||||||
|
const targetDeployInfo = JSON.parse(fs.readFileSync(`./deployinfo/${target}.deploy.json`).toString());
|
||||||
|
const srcKey = fs.readFileSync(`keypairs/${src}.key`).toString();
|
||||||
|
|
||||||
|
|
||||||
|
const srcSigner = new ethers.Wallet(srcKey).connect(
|
||||||
|
new ethers.providers.JsonRpcProvider(srcNetwork.rpc)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Attesting ${src} Network Token on ${target} Network`)
|
||||||
|
|
||||||
|
if(!address){
|
||||||
|
address = srcDeployInfo.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Attesting ${address} from ${target} network onto ${src}`);
|
||||||
|
|
||||||
|
const tx = await attestFromEth(
|
||||||
|
srcNetwork.tokenBridgeAddress,
|
||||||
|
srcSigner,
|
||||||
|
address,
|
||||||
|
{
|
||||||
|
gasLimit: 1500000
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// in this context the target is network we're attesting *from* so it's the network the vaa comes from (hence being placed as the 'source')
|
||||||
|
// The emitter for this is PORTAL, not our contract, so we set portal=true in fetchVaa
|
||||||
|
const attestVaa = await fetchVaa(src, tx, true);
|
||||||
|
|
||||||
|
switch(targetNetwork.type){
|
||||||
|
case "evm":
|
||||||
|
await createWrapped(target, src, attestVaa)
|
||||||
|
break;
|
||||||
|
case "solana":
|
||||||
|
await solana.createWrapped(target, src, attestVaa)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createWrapped(src:string, target: string, vaa:string){
|
||||||
|
const srcNetwork = config.networks[src];
|
||||||
|
const targetNetwork = config.networks[target];
|
||||||
|
const targetDeployInfo = JSON.parse(fs.readFileSync(`./deployinfo/${target}.deploy.json`).toString());
|
||||||
|
const key = fs.readFileSync(`keypairs/${src}.key`).toString();
|
||||||
|
const signer = new ethers.Wallet(key).connect(
|
||||||
|
new ethers.providers.JsonRpcProvider(srcNetwork.rpc)
|
||||||
|
);
|
||||||
|
const tx = await createWrappedOnEth(
|
||||||
|
srcNetwork.tokenBridgeAddress,
|
||||||
|
signer,
|
||||||
|
Buffer.from(vaa, 'base64'),
|
||||||
|
{
|
||||||
|
gasLimit: 1000000
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await new Promise((r) => setTimeout(r, 5000)); // wait for blocks to advance before fetching new foreign address
|
||||||
|
|
||||||
|
const foreignAddress = await getForeignAssetEth(
|
||||||
|
srcNetwork.tokenBridgeAddress,
|
||||||
|
signer,
|
||||||
|
targetNetwork.wormholeChainId,
|
||||||
|
tryNativeToUint8Array(targetDeployInfo.address, targetNetwork.wormholeChainId)
|
||||||
|
);
|
||||||
|
console.log(`${src} Network has new PortalWrappedToken for ${target} network at ${foreignAddress}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchVaa(src:string, tx:ethers.ethers.ContractReceipt, portal:boolean = false){
|
||||||
|
const srcNetwork = config.networks[src];
|
||||||
|
const srcDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${src}.deploy.json`).toString());
|
||||||
|
const seq = parseSequenceFromLogEth(tx, srcNetwork['bridgeAddress']);
|
||||||
|
let emitterAddr = "";
|
||||||
|
if(portal){
|
||||||
|
emitterAddr = getEmitterAddressEth(srcNetwork['tokenBridgeAddress']);
|
||||||
|
} else {
|
||||||
|
emitterAddr = getEmitterAddressEth(srcDeploymentInfo['address']);
|
||||||
|
}
|
||||||
|
await new Promise((r) => setTimeout(r, 5000)); //wait for Guardian to pick up message
|
||||||
|
console.log(
|
||||||
|
"Searching for: ",
|
||||||
|
`${config.wormhole.restAddress}/v1/signed_vaa/${srcNetwork.wormholeChainId}/${emitterAddr}/${seq}`
|
||||||
|
);
|
||||||
|
const vaaBytes = await (
|
||||||
|
await fetch(
|
||||||
|
`${config.wormhole.restAddress}/v1/signed_vaa/${srcNetwork.wormholeChainId}/${emitterAddr}/${seq}`
|
||||||
|
)
|
||||||
|
).json();
|
||||||
|
|
||||||
|
if(!vaaBytes['vaaBytes']){
|
||||||
|
throw new Error("VAA not found!");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("VAA Found: ", vaaBytes.vaaBytes);
|
||||||
|
return vaaBytes.vaaBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeVaa(src:string, vaa:string){
|
||||||
|
const srcDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${src}.deploy.json`).toString());
|
||||||
|
if(!srcDeploymentInfo['vaas']){
|
||||||
|
srcDeploymentInfo['vaas'] = [vaa]
|
||||||
|
} else {
|
||||||
|
srcDeploymentInfo['vaas'].push(vaa)
|
||||||
|
}
|
||||||
|
fs.writeFileSync(
|
||||||
|
`./deployinfo/${src}.deploy.json`,
|
||||||
|
JSON.stringify(srcDeploymentInfo, null, 4)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits target Purchase VAA onto src network
|
||||||
|
* @param src The EVM type of network that the VAA is being submitted to
|
||||||
|
* @param target The target network which initiated the purchase
|
||||||
|
* @param vaa The b64 encoded VAA
|
||||||
|
*/
|
||||||
|
export async function submitForeignPurchase(src:string, target:string, vaa:string) : Promise<string> {
|
||||||
|
const srcNetwork = config.networks[src];
|
||||||
|
const key = fs.readFileSync(`keypairs/${src}.key`).toString();
|
||||||
|
let srcDeploymentInfo;
|
||||||
|
let targetDeploymentInfo;
|
||||||
|
|
||||||
|
try{
|
||||||
|
srcDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${src}.deploy.json`).toString());
|
||||||
|
} catch (e){
|
||||||
|
throw new Error(`${src} is not deployed yet`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try{
|
||||||
|
targetDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${target}.deploy.json`).toString());
|
||||||
|
} catch (e){
|
||||||
|
throw new Error(`${target} is not deployed yet`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const signer = new ethers.Wallet(key).connect(
|
||||||
|
new ethers.providers.JsonRpcProvider(srcNetwork.rpc)
|
||||||
|
);
|
||||||
|
|
||||||
|
const messenger = new ethers.Contract(
|
||||||
|
srcDeploymentInfo.address,
|
||||||
|
ABI,
|
||||||
|
signer
|
||||||
|
);
|
||||||
|
|
||||||
|
//This will mint tokens and create a VAA to transfer them back over to the src chain
|
||||||
|
const tx = await messenger.submitForeignPurchase(Buffer.from(vaa, "base64"), {gasLimit: 1000000});
|
||||||
|
console.log("Submit foreign purchase succeeded");
|
||||||
|
|
||||||
|
//Even though we call our contract, portal=true here because our contract calls Portal's transfer() function, making the emitter Portal
|
||||||
|
const claimTokensVaa = await fetchVaa(src, await tx.wait(), true);
|
||||||
|
return claimTokensVaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claims the tokens generated on a foreign network onto the key in the source network
|
||||||
|
* @param src The chain you want to claim the vaa on
|
||||||
|
* @param vaa The vaa you want to claim
|
||||||
|
*/
|
||||||
|
export async function claimTokens(src:string, vaa:string){
|
||||||
|
const srcNetwork = config.networks[src];
|
||||||
|
const key = fs.readFileSync(`keypairs/${src}.key`).toString();
|
||||||
|
let srcDeploymentInfo;
|
||||||
|
|
||||||
|
try{
|
||||||
|
srcDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${src}.deploy.json`).toString());
|
||||||
|
} catch (e){
|
||||||
|
throw new Error(`${src} is not deployed yet`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const signer = new ethers.Wallet(key).connect(
|
||||||
|
new ethers.providers.JsonRpcProvider(srcNetwork.rpc)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Redeeming on ${src} network`);
|
||||||
|
await redeemOnEth(
|
||||||
|
srcNetwork.tokenBridgeAddress,
|
||||||
|
signer,
|
||||||
|
Buffer.from(vaa, "base64"),
|
||||||
|
{
|
||||||
|
gasLimit: 1000000
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function submitForeignSale(src:string, target:string, vaa:string){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a P3 VAA that can only be redeemed by target contract with src key as recipient address
|
||||||
|
* @param src
|
||||||
|
* @param target
|
||||||
|
* @param amount
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function buyToken(src:string, target: string, amount: number): Promise<string> {
|
||||||
|
//Buy Token on Target Chain with SRC Native Currency
|
||||||
|
// Create P3 VAA that pays X native and has the Receipient Address set to XMINT on Target Chain & payload is src key
|
||||||
|
const srcNetwork = config.networks[src];
|
||||||
|
const targetNetwork = config.networks[target];
|
||||||
|
const key = fs.readFileSync(`keypairs/${src}.key`).toString();
|
||||||
|
let srcDeploymentInfo;
|
||||||
|
let targetDeploymentInfo;
|
||||||
|
|
||||||
|
try{
|
||||||
|
srcDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${src}.deploy.json`).toString());
|
||||||
|
} catch (e){
|
||||||
|
throw new Error(`${src} is not deployed yet`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try{
|
||||||
|
targetDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${target}.deploy.json`).toString());
|
||||||
|
} catch (e){
|
||||||
|
throw new Error(`${target} is not deployed yet`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const signer = new ethers.Wallet(key).connect(
|
||||||
|
new ethers.providers.JsonRpcProvider(srcNetwork.rpc)
|
||||||
|
);
|
||||||
|
|
||||||
|
//For this project, 1 Native Token will always equal 100 Chain Tokens, no matter the source or target chains
|
||||||
|
const ethToTransferAmt = ethers.utils.parseUnits((amount/100).toString(), "18"); //how much native you want to transfer to buy AMT worth of Tokens on target chain
|
||||||
|
const targetChainAddress = tryNativeToUint8Array(targetDeploymentInfo.address, targetNetwork.wormholeChainId);
|
||||||
|
|
||||||
|
//The payload is just the purchaser's public key
|
||||||
|
// This is used to send a Payload 1 Transfer of Tokens back
|
||||||
|
// If it errors, will send a Refund VAA back
|
||||||
|
const purchaseOrderPayload = tryNativeToUint8Array(await signer.getAddress(), srcNetwork.wormholeChainId);
|
||||||
|
|
||||||
|
//Gotta approve the WETH transfer before we actually transfer using Token Bridge
|
||||||
|
const WETH = new ethers.Contract(
|
||||||
|
srcNetwork.wrappedNativeAddress,
|
||||||
|
JSON.parse(fs.readFileSync("./chains/evm/out/PortalWrappedToken.sol/PortalWrappedToken.json").toString()).abi,
|
||||||
|
signer
|
||||||
|
);
|
||||||
|
|
||||||
|
await WETH.approve(srcNetwork.tokenBridgeAddress, ethToTransferAmt);
|
||||||
|
console.log("WETH Approved");
|
||||||
|
|
||||||
|
// Now call token bridge to do the transfer
|
||||||
|
const tx = await transferFromEthNative(
|
||||||
|
srcNetwork.tokenBridgeAddress,
|
||||||
|
signer,
|
||||||
|
ethToTransferAmt,
|
||||||
|
targetNetwork.wormholeChainId,
|
||||||
|
targetChainAddress,
|
||||||
|
ethers.BigNumber.from(0),
|
||||||
|
{
|
||||||
|
gasPrice: 2000000000
|
||||||
|
},
|
||||||
|
purchaseOrderPayload
|
||||||
|
);
|
||||||
|
console.log("ETH Native Transferred");
|
||||||
|
|
||||||
|
// The buy order will be written to the SRC chain's vaa list
|
||||||
|
// Needs to be submitted to target chain with `submitForeignPurchase`
|
||||||
|
const vaa = await fetchVaa(src, tx, true);
|
||||||
|
writeVaa(src, vaa);
|
||||||
|
return vaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the balance of the SRC KEY on the SRC NETWORK for either SRC NATIVE CURRENCY or Target Tokens
|
||||||
|
* @param src
|
||||||
|
* @param target
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function balance(src:string, target: string) : Promise<string> {
|
||||||
|
const srcNetwork = config.networks[src];
|
||||||
|
const targetNetwork = config.networks[target];
|
||||||
|
const key = fs.readFileSync(`keypairs/${src}.key`).toString();
|
||||||
|
let srcDeploymentInfo;
|
||||||
|
let targetDeploymentInfo;
|
||||||
|
|
||||||
|
try{
|
||||||
|
srcDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${src}.deploy.json`).toString());
|
||||||
|
} catch (e){
|
||||||
|
throw new Error(`${src} is not deployed yet`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try{
|
||||||
|
targetDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${target}.deploy.json`).toString());
|
||||||
|
} catch (e){
|
||||||
|
throw new Error(`${target} is not deployed yet`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const signer = new ethers.Wallet(key).connect(
|
||||||
|
new ethers.providers.JsonRpcProvider(srcNetwork.rpc)
|
||||||
|
);
|
||||||
|
|
||||||
|
if(src == target){
|
||||||
|
//Get native currency balance
|
||||||
|
return (await signer.getBalance()).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else get the Token Balance of the Foreign Network's token on Src Network
|
||||||
|
const foreignAddress = await getForeignAssetEth(
|
||||||
|
srcNetwork.tokenBridgeAddress,
|
||||||
|
signer,
|
||||||
|
targetNetwork.wormholeChainId,
|
||||||
|
tryNativeToUint8Array(targetDeploymentInfo.address, targetNetwork.wormholeChainId)
|
||||||
|
);
|
||||||
|
|
||||||
|
const TKN = new ethers.Contract(
|
||||||
|
foreignAddress,
|
||||||
|
JSON.parse(fs.readFileSync("./chains/evm/out/PortalWrappedToken.sol/PortalWrappedToken.json").toString()).abi,
|
||||||
|
signer
|
||||||
|
);
|
||||||
|
return (<ethers.BigNumber>(await TKN.balanceOf(await signer.getAddress()))).div(1e8).toString()
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
|
||||||
|
export async function deploy(src: string){}
|
||||||
|
export async function attest(src: string, target:string, address:string = null){}
|
||||||
|
export async function registerApp(src:string, target:string){}
|
||||||
|
export async function createWrapped(src:string, target:string, vaa:string){}
|
||||||
|
export async function submitForeignPurchase(src:string, target:string, vaa:string){}
|
||||||
|
export async function submitForeignSale(src:string, target:string, vaa:string){}
|
||||||
|
export async function buyToken(src:string, target: string, amount:number){}
|
||||||
|
export async function sellToken(src:string, target:string, amount:number){}
|
|
@ -0,0 +1 @@
|
||||||
|
0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d
|
|
@ -0,0 +1 @@
|
||||||
|
0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d
|
|
@ -0,0 +1 @@
|
||||||
|
J2D4pwDred8P9ioyPEZVLPht885AeYpifsFGUyuzVmiKQosAvmZP4EegaKFrSprBC5vVP1xTvu61vYDWsxBNsYx
|
|
@ -0,0 +1,166 @@
|
||||||
|
import { Command } from 'commander';
|
||||||
|
import fs from 'fs';
|
||||||
|
import * as evm from './handlers/evm';
|
||||||
|
import * as solana from './handlers/solana';
|
||||||
|
|
||||||
|
const xmint = new Command();
|
||||||
|
const config = JSON.parse(fs.readFileSync('./xdapp.config.json').toString())
|
||||||
|
|
||||||
|
xmint
|
||||||
|
.name('xMint')
|
||||||
|
.description("Cross chain token minting, transfers, and widrawals.")
|
||||||
|
.version("0.1.0");
|
||||||
|
|
||||||
|
xmint
|
||||||
|
.command("deploy")
|
||||||
|
.description("Deploys on chain code.")
|
||||||
|
.argument("<src>", "the network you want to deply")
|
||||||
|
.action(async (src) => {
|
||||||
|
if(!config.networks[src]){
|
||||||
|
console.error(`ERROR: ${src} not found in xdapp.config.json`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`Deploying ${src}...`);
|
||||||
|
|
||||||
|
switch(config.networks[src].type){
|
||||||
|
case "evm":
|
||||||
|
await evm.deploy(src);
|
||||||
|
break;
|
||||||
|
case "solana":
|
||||||
|
await solana.deploy(src);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Deploy finished!`);
|
||||||
|
});
|
||||||
|
|
||||||
|
xmint
|
||||||
|
.command("register-app")
|
||||||
|
.description("Registers the target app and target token with the source on chain app")
|
||||||
|
.argument("<src>", "the network you want to register the app on")
|
||||||
|
.argument("<target>", "the network you want to register")
|
||||||
|
.action(async (src, target) => {
|
||||||
|
if(!config.networks[src]){
|
||||||
|
console.error(`ERROR: ${src} not found in xdapp.config.json`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!config.networks[target]){
|
||||||
|
console.error(`ERROR: ${target} not found in xdapp.config.json`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let srcHandler;
|
||||||
|
switch(config.networks[src].type){
|
||||||
|
case "evm":
|
||||||
|
srcHandler = evm;
|
||||||
|
break;
|
||||||
|
case "solana":
|
||||||
|
srcHandler = solana;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Registering ${target} app and token onto ${src} network`)
|
||||||
|
await srcHandler.registerApp(src,target)
|
||||||
|
try{
|
||||||
|
console.log(`Attesting ${src} Wrapped Native to ${target}`);
|
||||||
|
await srcHandler.attest(target, src, config.networks[src].wrappedNativeAddress)
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Wrapped Native attestion exists already")
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${target} contract address was registered on ${src} and ${target} token was attested to ${src}`)
|
||||||
|
});
|
||||||
|
|
||||||
|
xmint
|
||||||
|
.command("buy-token")
|
||||||
|
.description("buy token using the src network's native currency and bridge it back to src network and hold as bridged funds")
|
||||||
|
.argument("<src>", "the network with the native token you want to spend")
|
||||||
|
.argument("<target>", "the network whose xmint token you want to buy")
|
||||||
|
.argument("<amt>", "amount of token to buy. Always 1 Native : 100 TOKEN")
|
||||||
|
.action(async (src, target, amt) => {
|
||||||
|
if(!config.networks[src]){
|
||||||
|
console.error(`ERROR: ${src} not found in xdapp.config.json`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!config.networks[target]){
|
||||||
|
console.error(`ERROR: ${target} not found in xdapp.config.json`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(isNaN(parseInt(amt))){
|
||||||
|
console.error(`Error: Invalid Amount!`)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buy Token On Source
|
||||||
|
// SubmitForeignPurchase on Target
|
||||||
|
// Claim Tokens on Source
|
||||||
|
let srcHandler;
|
||||||
|
switch(config.networks[src].type){
|
||||||
|
case "evm":
|
||||||
|
srcHandler = evm;
|
||||||
|
break;
|
||||||
|
case "solana":
|
||||||
|
srcHandler = solana;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetHandler;
|
||||||
|
switch(config.networks[target].type){
|
||||||
|
case "evm":
|
||||||
|
targetHandler = evm;
|
||||||
|
break;
|
||||||
|
case "solana":
|
||||||
|
targetHandler = solana;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Creating Buy VAA on ${src} network...`);
|
||||||
|
const buyVAA = await srcHandler.buyToken(src, target, parseInt(amt));
|
||||||
|
console.log(`Submitting buy vaa on ${target} network...`);
|
||||||
|
const claimTokensVAA = await targetHandler.submitForeignPurchase(target, src, buyVAA);
|
||||||
|
console.log(`Claiming tokens on ${src} network...`);
|
||||||
|
await srcHandler.claimTokens(src, claimTokensVAA);
|
||||||
|
|
||||||
|
console.log(`Purchase of ${amt} of ${target} network's tokens complete.`)
|
||||||
|
});
|
||||||
|
|
||||||
|
xmint
|
||||||
|
.command("sell-token")
|
||||||
|
.description("sells token from target network back to target network for src network's native token")
|
||||||
|
.argument("<src>", "the network whose native currency you want to receive")
|
||||||
|
.argument("<target>", "the target network whose token you want to sell")
|
||||||
|
.argument("<amt>", "amount of token to sell. Always 1 TOKEN : 0.01 NATIVE")
|
||||||
|
.action(async (src, targe, amt) => {
|
||||||
|
// Sell Token on Source
|
||||||
|
// SubmitForeignSale on Target
|
||||||
|
// Claim Native
|
||||||
|
});
|
||||||
|
|
||||||
|
xmint
|
||||||
|
.command("balance")
|
||||||
|
.description("gets the balance of the target networks tokens for src network's keys. Pass in src src if you want find native tokens balance on src network")
|
||||||
|
.argument("<src>", "the network whose key you want to lookup the balance for")
|
||||||
|
.argument("<target>", "the network whose tokens you want to look up the balance for. Pass in src network again to get native token balance. ")
|
||||||
|
.action(async (src, target) => {
|
||||||
|
if(!config.networks[src]){
|
||||||
|
console.error(`ERROR: ${src} not found in xdapp.config.json`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!config.networks[target]){
|
||||||
|
console.error(`ERROR: ${target} not found in xdapp.config.json`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let balance;
|
||||||
|
switch(config.networks[src].type){
|
||||||
|
case "evm":
|
||||||
|
balance = await evm.balance(src, target);
|
||||||
|
break;
|
||||||
|
case "solana":
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Balance of ${src} key for ${target} tokens is ${balance}`);
|
||||||
|
})
|
||||||
|
|
||||||
|
xmint.parse();
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "xmint",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Multichain P3 Example",
|
||||||
|
"main": "orchestrator.ts",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@certusone/wormhole-sdk": "^0.6.1",
|
||||||
|
"@project-serum/anchor": "^0.25.0",
|
||||||
|
"@types/node": "^18.7.1",
|
||||||
|
"@types/node-fetch": "^2.6.2",
|
||||||
|
"commander": "^9.4.0",
|
||||||
|
"ethers": "^5.6.9",
|
||||||
|
"node-fetch": "2"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Deploy the code
|
||||||
|
ts-node orchestrator.ts deploy evm0
|
||||||
|
ts-node orchestrator.ts deploy evm1
|
||||||
|
|
||||||
|
# Register Apps
|
||||||
|
ts-node orchestrator.ts register-app evm0 evm1
|
||||||
|
ts-node orchestrator.ts register-app evm1 evm0
|
||||||
|
|
||||||
|
# Print Balances
|
||||||
|
ts-node orchestrator.ts balance evm0 evm0
|
||||||
|
ts-node orchestrator.ts balance evm0 evm1
|
||||||
|
ts-node orchestrator.ts balance evm1 evm1
|
||||||
|
ts-node orchestrator.ts balance evm1 evm0
|
||||||
|
|
||||||
|
# Buy Tokens
|
||||||
|
ts-node orchestrator.ts buy-token evm0 evm1 100
|
||||||
|
|
||||||
|
# Print Balance
|
||||||
|
ts-node orchestrator.ts balance evm0 evm0
|
||||||
|
ts-node orchestrator.ts balance evm0 evm1
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Deploy the code on EVM0 and SOL0
|
||||||
|
ts-node orchestrator.ts deploy evm0
|
||||||
|
ts-node orchestrator.ts deploy sol0
|
||||||
|
|
||||||
|
# Print Balances for EVM0 and SOL0 Keypairs
|
||||||
|
ts-node orchestrator.ts balance evm0 evm0
|
||||||
|
ts-node orchestrator.ts balance evm0 sol0
|
||||||
|
ts-node orchestrator.ts balance sol0 sol0
|
||||||
|
ts-node orchestrator.ts balance sol0 evm0
|
||||||
|
|
||||||
|
# Register Apps EVM<>SOL
|
||||||
|
ts-node orchestrator.ts register-app evm0 sol0
|
||||||
|
ts-node orchestrator.ts register-app sol0 evm0
|
||||||
|
|
||||||
|
# Buy SOL0-TOKEN with eth
|
||||||
|
ts-node orchestrator.ts buy-token evm0 sol0 100
|
||||||
|
# Print SOL0 Balance
|
||||||
|
ts-node orchestrator.ts balance evm0 evm0
|
||||||
|
ts-node orchestrator.ts balance evm0 sol0
|
||||||
|
|
||||||
|
# Buy EVM0-TOKEN with sol
|
||||||
|
ts-node orchestrator.ts buy-token sol0 evm0 100
|
||||||
|
# Print SOL0 Balance for Solana and EVM0-TOKENS
|
||||||
|
ts-node orchestrator.ts balance sol0 sol0
|
||||||
|
ts-node orchesatrator.ts balance sol0 evm0
|
||||||
|
|
||||||
|
# Sell SOL0-TOKEN for eth
|
||||||
|
ts-node orchestrator.ts sell-token sol0 evm0 100
|
||||||
|
# Print SOL0 Balance for solana and EVM0-TOKENS
|
||||||
|
ts-node orchestrator.ts balance sol0 sol0
|
||||||
|
ts-node orchestrator.ts balance sol0 evm0
|
||||||
|
|
||||||
|
# Sell EVM0-TOKEN for solana
|
||||||
|
ts-node orchestrator.ts sell-token evm0 sol0 100
|
||||||
|
# Print EVM0 Balance for eth and SOL0-TOKENS
|
||||||
|
ts-node orchestrator.ts balance evm0 evm0
|
||||||
|
ts-node orchestrator.ts balance evm0 sol0
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["node"],
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"esModuleInterop": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"networks": {
|
||||||
|
"evm0": {
|
||||||
|
"type": "evm",
|
||||||
|
"wormholeChainId": 2,
|
||||||
|
"rpc": "http://localhost:8545",
|
||||||
|
"bridgeAddress": "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550",
|
||||||
|
"tokenBridgeAddress": "0x0290FB167208Af455bB137780163b7B7a9a10C16",
|
||||||
|
"wrappedNativeAddress": "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"
|
||||||
|
},
|
||||||
|
"evm1": {
|
||||||
|
"type": "evm",
|
||||||
|
"wormholeChainId": 4,
|
||||||
|
"rpc": "http://localhost:8546",
|
||||||
|
"bridgeAddress": "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550",
|
||||||
|
"tokenBridgeAddress": "0x0290FB167208Af455bB137780163b7B7a9a10C16",
|
||||||
|
"wrappedNativeAddress": "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"
|
||||||
|
},
|
||||||
|
"sol0": {
|
||||||
|
"type": "solana",
|
||||||
|
"wormholeChainId": 1,
|
||||||
|
"rpc": "http://localhost:8899",
|
||||||
|
"bridgeAddress": "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o",
|
||||||
|
"tokenBridgeAddress": "B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE",
|
||||||
|
"wrappedNativeAddress": "So11111111111111111111111111111111111111112"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"wormhole": {
|
||||||
|
"restAddress": "http://localhost:7071"
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue