diff --git a/.gitignore b/.gitignore index 82e68e79dd2..675ca1c713e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +**/*repomix* + node_modules test_deploy.env diff --git a/.gitmodules b/.gitmodules index d5392fba8eb..43dda366eb8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,9 @@ [submodule "solidity/lib/fx-portal"] path = solidity/lib/fx-portal url = https://github.com/0xPolygon/fx-portal +[submodule "dymension/foo_test/foundry/lib/forge-std"] + path = dymension/foo_test/foundry/lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "dymension/foo_test/foundry/lib/openzeppelin-contracts"] + path = dymension/foo_test/foundry/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/dymension/dymension.readme.txt b/dymension/dymension.readme.txt new file mode 100644 index 00000000000..ef6d053f350 --- /dev/null +++ b/dymension/dymension.readme.txt @@ -0,0 +1,61 @@ +Testing + yarn test:forge --match-contract HypERC20MemoTest + yarn test:forge --match-contract HypERC20CollateralMemoTest + yarn test:forge --match-contract HypNativeMemoTest + cargo test --test functional + +Notes + Ethereum + HypERC20 = Synthetic + HypERC20Collateral = Collateral + HypNative = Native + Solana + hyperlane-sealevel-token = Synthetic + hyperlane-sealevel-token-collateral = Collateral + hyperlane-sealevel-token-native = Native + +Change list + Ethereum + Copied HypERC20 and modified to include memo in transferFromSender + Copied test for HypERC20 and added memo check + Copied HypNative and modified to include memo in transferFromSender + Copied test for HypNative and added memo check + Extended HypERC20Collateral with override to include memo in transferFromSender + Copied test for HypeERC20Collateral and added memo check + Solana + Added hyperlane-sealevel-token-native-memo + Added hyperlane-sealevel-token-collateral-memo + Added hyperlane-sealevel-token-memo + +Improvements to be made (possibly) + Ethereum + Just have a method to do both the memo setting and the transfer remote + Solana + Find a way to remove the plugin.rs duplication in the tokens + +How to work on the typescript CLI: + The CLI depends on the SDK, so first do yarn build from typescript/sdk, and only then will yarn build from typescript/cli work + How to rebuild and reinstall? + # in top level (hyperlane-monorepo) + yarn clean; yarn build; # CLEAN IS VERY IMPORTANT! + #in typescript/cli + npm uninstall -g @hyperlane-xyz/cli; + yarn install + yarn build + yarn bundle + npm install -g + hyperlane --version + +How to build rust agents: + cd rust/main + cargo build --release --bin relayer + cargo build --release --bin validator + +How to manage solana versions: + # initial install + curl --proto '=https' --tlsv1.2 -sSfL https://solana-install.solana.workers.dev | bash + # then agave-install or solana-install + solana-install init v1.18.18 + solana-install init v1.14.20 + sh -c "$(curl -sSfL https://release.anza.xyz/v2.2.13/install)" + diff --git a/dymension/ethereum_test/.gitignore b/dymension/ethereum_test/.gitignore new file mode 100644 index 00000000000..c036379422e --- /dev/null +++ b/dymension/ethereum_test/.gitignore @@ -0,0 +1 @@ +tmp/ \ No newline at end of file diff --git a/dymension/ethereum_test/chains/anvil0/metadata.yaml b/dymension/ethereum_test/chains/anvil0/metadata.yaml new file mode 100644 index 00000000000..b21f4d658d0 --- /dev/null +++ b/dymension/ethereum_test/chains/anvil0/metadata.yaml @@ -0,0 +1,14 @@ +# yaml-language-server: $schema=../schema.json +chainId: 31337 +displayName: Anvil0 +domainId: 31337 +isTestnet: true +name: anvil0 +nativeToken: + decimals: 18 + name: Ether + symbol: ETH +protocol: ethereum +rpcUrls: + - http: http://localhost:8545 +technicalStack: other diff --git a/dymension/ethereum_test/chains/dymension/metadata.yaml b/dymension/ethereum_test/chains/dymension/metadata.yaml new file mode 100644 index 00000000000..aacece47fe2 --- /dev/null +++ b/dymension/ethereum_test/chains/dymension/metadata.yaml @@ -0,0 +1,41 @@ +# yaml-language-server: $schema=../schema.json +# see https://github.com/hyperlane-xyz/hyperlane-registry/blob/main/chains/kyvetestnet/metadata.yaml +bech32Prefix: dym +# todo can add block explorers +blocks: + confirmations: 1 + estimateBlockTime: 6 + reorgPeriod: 1 +canonicalAsset: adym +chainId: dymension_100-1 +contractAddressBytes: 32 +deployer: + name: DYMENSION + url: https://example.com # TODO +displayName: Dymension Hub Local +gasCurrencyCoinGeckoId: dymension +# Generated from: console.log(parseInt('0x'+Buffer.from('DYMENSION').toString('hex'))) +domainId: 1260813472 +gasPrice: + amount: "100000000000.0" + denom: adym +grpcUrls: + - http: http://127.0.0.1:8090 +index: # TODO ?? + from: 10 # low block start +isTestnet: true +name: dymension +nativeToken: + decimals: 18 + denom: adym + name: ADYM # dym or adym + symbol: ADYM # dym or adym +protocol: cosmosnative +restUrls: + - http: http://localhost:1318 +rpcUrls: # (JSON COMET ONE, e.g. /block?height) + - http: http://localhost:36657 +slip44: 118 #?? +technicalStack: other +transactionOverrides: + gasPrice: "2.0" # TODO: ?? \ No newline at end of file diff --git a/dymension/ethereum_test/commands.sh b/dymension/ethereum_test/commands.sh new file mode 100644 index 00000000000..74f74d11670 --- /dev/null +++ b/dymension/ethereum_test/commands.sh @@ -0,0 +1,196 @@ +export BASE_PATH="/Users/danwt/Documents/dym/d-hyperlane-monorepo" +export HUB_BASE_PATH="/Users/danwt/Documents/dym/d-dymension/scripts/hyperlane_test" + +######################################################################################### +######################################################################################### +# Q: What is this? +# A: Some commands to run Dymension Hub + Anvil instance and connect them and relay between them +# Scenario: Dymension Hub will have collateral ADYM and Anvil will have synthetic memo +######################################################################################### + +# clean slate +trash ~/.hyperlane; trash ~/.dymension + +############################################################################################## +############################################################################################## +# PART 1: Start chains and deploy contracts + +################ +# ENV: + +source $HUB_BASE_PATH/env.sh + +export HYP_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +export HYP_ADDR="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" +export HYP_ADDR_ZEROS="0x000000000000000000000000f39Fd6e51aad88F6F4ce6aB8827279cffFb92266" # this is zero padded regular address +export RELAYER_ADDR="dym15428vq2uzwhm3taey9sr9x5vm6tk78ewtfeeth" # relayer derives from HYP_KEY +export HUB_DOMAIN=1260813472 +export ETH_DOMAIN=31337 +export DENOM="adym" + +################ +# START NODES: + +# HUB +cd dymension/ # hub repo +bash scripts/setup_local.sh +dymd start --log_level=debug +# see ping pub for explorer + +# ETHEREUM +anvil --port 8545 --chain-id 31337 --block-time 1 # make sure rollapp-evm not listening on same port +# see otterscan for explorer + +################# +# DEPLOY HYPERLANE CORE TO ETH: +cd hyperlane-monorepo/dymension/ethereum_test + +trash ~/.hyperlane; mkdir ~/.hyperlane; cp -r chains ~/.hyperlane/chains; + +# only deploy anvil0, without block explorer +hyperlane core deploy + +################ +# HUB: + +hub tx hyperlane ism create-noop "${HUB_FLAGS[@]}" +sleep 7; +ISM=$(curl -s http://localhost:1318/hyperlane/v1/isms | jq '.isms.[0].id' -r); echo $ISM; + +hub tx hyperlane hooks noop create "${HUB_FLAGS[@]}" +sleep 7; +NOOP_HOOK=$(curl -s http://localhost:1318/hyperlane/v1/noop_hooks | jq '.noop_hooks.[0].id' -r); echo $NOOP_HOOK; + +hub tx hyperlane mailbox create $ISM $HUB_DOMAIN "${HUB_FLAGS[@]}" +sleep 7; +MAILBOX=$(curl -s http://localhost:1318/hyperlane/v1/mailboxes | jq '.mailboxes.[0].id' -r); echo $MAILBOX; + +hub tx hyperlane hooks merkle create $MAILBOX "${HUB_FLAGS[@]}" +sleep 7; +MERKLE_HOOK=$(curl -s http://localhost:1318/hyperlane/v1/merkle_tree_hooks | jq '.merkle_tree_hooks.[0].id' -r); echo $MERKLE_HOOK; + +# update mailbox again. default hook (e.g. IGP), required hook (e.g. merkle tree) +hub tx hyperlane mailbox set $MAILBOX --default-hook $NOOP_HOOK --required-hook $MERKLE_HOOK "${HUB_FLAGS[@]}" + +hub tx hyperlane-transfer create-collateral-token $MAILBOX $DENOM "${HUB_FLAGS[@]}" +sleep 7; +TOKEN_ID=$(curl -s http://localhost:1318/hyperlane/v1/tokens | jq '.tokens.[0].id' -r); echo $TOKEN_ID + +################ +# ANVIL: + +# cd hyperlane-monorepo/dymension/ethereum_test + +# populate addresses https://github.com/hyperlane-xyz/hyperlane-registry/blob/main/chains/kyvetestnet/addresses.yaml +touch ~/.hyperlane/chains/dymension/addresses.yaml +dasel put -f ~/.hyperlane/chains/dymension/addresses.yaml 'interchainGasPaymaster' -v $NOOP_HOOK +dasel put -f ~/.hyperlane/chains/dymension/addresses.yaml 'interchainSecurityModule' -v $ISM +dasel put -f ~/.hyperlane/chains/dymension/addresses.yaml 'mailbox' -v $MAILBOX +dasel put -f ~/.hyperlane/chains/dymension/addresses.yaml 'merkleTreeHook' -v $MERKLE_HOOK +dasel put -f ~/.hyperlane/chains/dymension/addresses.yaml 'validatorAnnounce' -v $MAILBOX +# then manually add quotes to the addresses (!!) + +# also, check configs/warp-route-deployment.yaml matches +dasel put -f configs/warp-route-deployment.yaml 'dymension.token' -v $TOKEN_ID +dasel put -f configs/warp-route-deployment.yaml 'dymension.foreignDeployment' -v $TOKEN_ID +dasel put -f configs/warp-route-deployment.yaml 'dymension.mailbox' -v $MAILBOX +# then manually add quotes to the addresses (!!) + +# now use hyperlane CLI to deploy only the contracts needed on anvil, making use of a foreign deployment config for dymension side +# it will say to deploy to dymension too, but it won't +hyperlane warp deploy + +################ +# FINISH HUB SETUP: + +ETH_TOKEN_CONTRACT_RAW=$(dasel -f ~/.hyperlane/deployments/warp_routes/ADYM/anvil0-config.yaml -r yaml 'tokens.index(0).addressOrDenom'); echo $ETH_TOKEN_CONTRACT_RAW; +# manual step TODO: automate +ETH_TOKEN_CONTRACT="0x0000000000000000000000004A679253410272dd5232B3Ff7cF5dbB88f295319" # Need to zero pad it! (with 0x000000000000000000000000) + +hub tx hyperlane-transfer enroll-remote-router $TOKEN_ID $ETH_DOMAIN $ETH_TOKEN_CONTRACT 0 "${HUB_FLAGS[@]}" # gas = 0 +sleep 7; +curl -s http://localhost:1318/hyperlane/v1/tokens/$TOKEN_ID/remote_routers # check + +############################################################################################## +############################################################################################## +# PART 1: SETUP RELAYERS AND VALIDATORS +# https://docs.hyperlane.xyz/docs/guides/deploy-hyperlane-local-agents +# build agent binaries if needed + +MONO_WORKING_DIR=/Users/danwt/Documents/dym/d-hyperlane-monorepo/dymension/ethereum_test +RELAYER_DB=$MONO_WORKING_DIR/tmp/hyperlane_db_relayer +trash $MONO_WORKING_DIR/tmp/ +mkdir $MONO_WORKING_DIR/tmp/ + +################################# +# RELAYING +# https://docs.hyperlane.xyz/docs/operate/relayer/run-relayer + +# regen config +cd hyperlane-monorepo/dymension/ethereum_test +hyperlane registry agent-config --chains anvil0,dymension # DO NOT USE, DOES NOT PROPERLY INCLUDE GRPC URLS, USE PRECONFIGURED + +export CONFIG_FILES=$MONO_WORKING_DIR/configs/agent-config.json +# see reference https://docs.hyperlane.xyz/docs/operate/config-reference#config_files + +cd rust/main + +# need to fund relayer +dymd tx bank send hub-user $RELAYER_ADDR 1000000000000000000000adym "${HUB_FLAGS[@]}" + +./target/release/relayer \ + --db $RELAYER_DB \ + --relayChains anvil0,dymension \ + --allowLocalCheckpointSyncers true \ + --defaultSigner.key $HYP_KEY \ + --metrics-port 9091 \ + --chains.dymension.signer.type cosmosKey \ + --chains.dymension.signer.prefix dym \ + --chains.dymension.signer.key $HYP_KEY \ + --log.level debug + +################################# +# DO A TRANSFER HUB -> ETHEREUM + +AMT=1000 +hub tx hyperlane-transfer transfer $TOKEN_ID $ETH_DOMAIN $HYP_ADDR_ZEROS $AMT "${HUB_FLAGS[@]}" --max-hyperlane-fee 1000adym --gas-limit 10000000000 +sleep 5; +curl -s http://localhost:1318/hyperlane/v1/tokens/$TOKEN_ID/bridged_supply + +# If relaying worked, should have amt tokens here +cast call $ETH_TOKEN_CONTRACT_RAW "balanceOf(address)(uint256)" $HYP_ADDR --rpc-url http://localhost:8545 + +# fund relayer +dymd tx bank send hub-user $RELAYER_ADDR 1000000000000000000000adym "${HUB_FLAGS[@]}" + +HUB_RECEIVER_ADDR_NATIVE="dym1yvq7swunxwduq5kkmuftqccxgqk3f6nsaf3sqz" +HUB_RECEIVER_ADDR=$(dymd q forward hl-eth-recipient $HUB_RECEIVER_ADDR_NATIVE) +# args are destination, recipient, amount +AMT=5 +DEMO_MEMO="0x68656c6c6f" # 'hello' +cast send $ETH_TOKEN_CONTRACT_RAW "transferRemoteMemo(uint32,bytes32,uint256,bytes)" $HUB_DOMAIN $HUB_RECEIVER_ADDR $AMT $DEMO_MEMO --private-key $HYP_KEY --rpc-url http://localhost:8545 +# note: if using a native token type on ethereum, need to also send some eth ('--value 1') + +# confirm tx has memo event on hub +# confirm tx has memo event on anvil +cast call $ETH_TOKEN_CONTRACT_RAW "memoOf(bytes32)(bytes)" $HUB_RECEIVER_ADDR --rpc-url http://localhost:8545 + +dymd q bank balances $HUB_RECEIVER_ADDR_NATIVE + +############################################################################################## +############################################################################################## +# APPENDIX: DEBUGGING + +# eth balance: +cast balance $HYP_ADDR --rpc-url http://localhost:8545 + +# Explorer, uses https://github.com/otterscan/otterscan +docker pull otterscan/otterscan:latest +docker run -p 5100:80 \ + -e OTTERSCAN_RPC_URL="http://host.docker.internal:8545" \ + otterscan/otterscan:latest +# visit http://localhost:5100/ + +# Hub: https://github.com/ping-pub/explorer +yarn --ignore-engines && yarn serve +# visit http://localhost:5173/ diff --git a/dymension/ethereum_test/configs/.gitignore b/dymension/ethereum_test/configs/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dymension/ethereum_test/configs/agent-config.json b/dymension/ethereum_test/configs/agent-config.json new file mode 100644 index 00000000000..789d22c1aee --- /dev/null +++ b/dymension/ethereum_test/configs/agent-config.json @@ -0,0 +1,98 @@ +{ + "chains": { + "anvil0": { + "chainId": 31337, + "displayName": "Anvil0", + "domainId": 31337, + "isTestnet": true, + "name": "anvil0", + "nativeToken": { + "decimals": 18, + "name": "Ether", + "symbol": "ETH" + }, + "protocol": "ethereum", + "rpcUrls": [ + { + "http": "http://localhost:8545" + } + ], + "technicalStack": "other", + "domainRoutingIsmFactory": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", + "interchainAccountIsm": "0x9A676e781A523b5d0C0e43731313A708CB607508", + "interchainAccountRouter": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed", + "mailbox": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", + "merkleTreeHook": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e", + "proxyAdmin": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", + "staticAggregationHookFactory": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", + "staticAggregationIsmFactory": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "staticMerkleRootMultisigIsmFactory": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "staticMerkleRootWeightedMultisigIsmFactory": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", + "staticMessageIdMultisigIsmFactory": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "staticMessageIdWeightedMultisigIsmFactory": "0x0165878A594ca255338adfa4d48449f69242Eb8F", + "testRecipient": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d", + "validatorAnnounce": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c", + "interchainGasPaymaster": "0x0000000000000000000000000000000000000000", + "index": { + "from": 65 + } + }, + "dymension": { + "bech32Prefix": "dym", + "blocks": { + "confirmations": 1, + "estimateBlockTime": 6, + "reorgPeriod": 1 + }, + "canonicalAsset": "adym", + "chainId": "dymension_100-1", + "contractAddressBytes": 32, + "deployer": { + "name": "DYMENSION", + "url": "https://example.com" + }, + "displayName": "Dymension Hub Local", + "gasCurrencyCoinGeckoId": "dymension", + "domainId": 1260813472, + "gasPrice": { + "amount": "100000000000.0", + "denom": "adym" + }, + "index": { + "from": 10 + }, + "grpcUrls": [ + { + "http": "http://127.0.0.1:8090" + } + ], + "isTestnet": true, + "name": "dymension", + "nativeToken": { + "decimals": 18, + "denom": "adym", + "name": "ADYM", + "symbol": "ADYM" + }, + "protocol": "cosmosnative", + "restUrls": [ + { + "http": "http://localhost:1318" + } + ], + "rpcUrls": [ + { + "http": "http://localhost:36657" + } + ], + "slip44": 118, + "technicalStack": "other", + "interchainGasPaymaster": "0x726f757465725f706f73745f6469737061746368000000000000000000000000", + "interchainSecurityModule": "0x726f757465725f69736d00000000000000000000000000000000000000000000", + "mailbox": "0x68797065726c616e650000000000000000000000000000000000000000000000", + "merkleTreeHook": "0x726f757465725f706f73745f6469737061746368000000030000000000000001", + "validatorAnnounce": "0x68797065726c616e650000000000000000000000000000000000000000000000" + } + }, + "defaultRpcConsensusType": "fallback" +} diff --git a/dymension/ethereum_test/configs/core-config.yaml b/dymension/ethereum_test/configs/core-config.yaml new file mode 100644 index 00000000000..05730247796 --- /dev/null +++ b/dymension/ethereum_test/configs/core-config.yaml @@ -0,0 +1,14 @@ +defaultHook: + type: merkleTreeHook +defaultIsm: + relayer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + type: trustedRelayerIsm +owner: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" +proxyAdmin: + owner: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" +requiredHook: + beneficiary: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + maxProtocolFee: "100000000000000000" + owner: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + protocolFee: "0" + type: protocolFee diff --git a/dymension/ethereum_test/configs/readme.md b/dymension/ethereum_test/configs/readme.md new file mode 100644 index 00000000000..1bc53ee7935 --- /dev/null +++ b/dymension/ethereum_test/configs/readme.md @@ -0,0 +1,3 @@ +agent-config.json is for relayer. See https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/main/config/testnet_config.json cosmosnative protocols for examples +core-config.yaml is for the core contracts on anvil, we manually create it, but you can also use the cli tool (core init) +warp-route-deployment.yaml is manually created and it's just enough to deploy the anvil warp route contracts. The extra stuff for dymension is just a workaround diff --git a/dymension/ethereum_test/configs/warp-route-deployment.yaml b/dymension/ethereum_test/configs/warp-route-deployment.yaml new file mode 100644 index 00000000000..3d6e918444c --- /dev/null +++ b/dymension/ethereum_test/configs/warp-route-deployment.yaml @@ -0,0 +1,17 @@ +anvil0: + interchainSecurityModule: "0x0000000000000000000000000000000000000000" + owner: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + type: syntheticMemo + name: aDym + symbol: ADYM + decimals: 18 + isNft: false + gas: 1 +dymension: + foreignDeployment: "0x726f757465725f61707000000000000000000000000000fe0000000000000000" + mailbox: "0x68797065726c616e650000000000000000000000000000000000000000000000" + owner: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + type: collateralMemo + token: "0x726f757465725f61707000000000000000000000000000fe0000000000000000" + interchainSecurityModule: "0x0000000000000000000000000000000000000000" + gas: 1 diff --git a/dymension/ethereum_test/readme.md b/dymension/ethereum_test/readme.md new file mode 100644 index 00000000000..3a930d88e0f --- /dev/null +++ b/dymension/ethereum_test/readme.md @@ -0,0 +1 @@ +This yarn project is NOT intended to be a subproject of the monorepo yarn project. It's a one-off. diff --git a/dymension/foo_test/.gitignore b/dymension/foo_test/.gitignore new file mode 100644 index 00000000000..c036379422e --- /dev/null +++ b/dymension/foo_test/.gitignore @@ -0,0 +1 @@ +tmp/ \ No newline at end of file diff --git a/dymension/foo_test/commands.sh b/dymension/foo_test/commands.sh new file mode 100644 index 00000000000..d074aa7cde7 --- /dev/null +++ b/dymension/foo_test/commands.sh @@ -0,0 +1,73 @@ +trash ~/.hyperlane; trash ~/.dymension +anvil --port 8545 --chain-id 31337 --block-time 1 + +mkdir ~/.hyperlane; cp -r /Users/danwt/Documents/dym/d-hyperlane-monorepo/dymension/ethereum_test/chains ~/.hyperlane/chains + +export HYP_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + +cp /Users/danwt/Documents/dym/d-hyperlane-monorepo/dymension/ethereum_test/configs/core-config.yaml configs + +hyperlane core deploy + +# run steps from ethereum_test/commands.sh, up to but not including token + +hub tx hyperlane-transfer create-synthetic-token $MAILBOX "${HUB_FLAGS[@]}" +sleep 7; +TOKEN_ID=$(curl -s http://localhost:1318/hyperlane/v1/tokens | jq '.tokens.[0].id' -r); echo $TOKEN_ID + + +touch ~/.hyperlane/chains/dymension/addresses.yaml +dasel put -f ~/.hyperlane/chains/dymension/addresses.yaml 'interchainGasPaymaster' -v $NOOP_HOOK +dasel put -f ~/.hyperlane/chains/dymension/addresses.yaml 'interchainSecurityModule' -v $ISM +dasel put -f ~/.hyperlane/chains/dymension/addresses.yaml 'mailbox' -v $MAILBOX +dasel put -f ~/.hyperlane/chains/dymension/addresses.yaml 'merkleTreeHook' -v $MERKLE_HOOK +dasel put -f ~/.hyperlane/chains/dymension/addresses.yaml 'validatorAnnounce' -v $MAILBOX + +dasel put -f configs/warp-route-deployment.yaml 'dymension.token' -v $TOKEN_ID +dasel put -f configs/warp-route-deployment.yaml 'dymension.foreignDeployment' -v $TOKEN_ID +dasel put -f configs/warp-route-deployment.yaml 'dymension.mailbox' -v $MAILBOX + +cd foundry/ +forge script script/Foo.s.sol:DeployFoo --rpc-url http://localhost:8545 --private-key $HYP_KEY --broadcast +# put address in warp-route-deployment.yaml anvil0.token + +hyperlane warp deploy + +FOO_TOKEN_CONTRACT_RAW=0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1 +COLLAT_TOKEN_CONTRACT_RAW=$(dasel -f ~/.hyperlane/deployments/warp_routes/FOO/anvil0-config.yaml -r yaml 'tokens.index(0).addressOrDenom'); echo $ETH_TOKEN_CONTRACT_RAW; +# TODO: derive +COLLAT_TOKEN_CONTRACT="0x00000000000000000000000084eA74d481Ee0A5332c457a4d796187F6Ba67fEB" # Need to zero pad it! (with 0x000000000000000000000000) + +hub tx hyperlane-transfer enroll-remote-router $TOKEN_ID $ETH_DOMAIN $COLLAT_TOKEN_CONTRACT 0 "${HUB_FLAGS[@]}" # gas = 0 + +trash /Users/danwt/Documents/dym/d-hyperlane-monorepo/dymension/foo_test/tmp/ +mkdir /Users/danwt/Documents/dym/d-hyperlane-monorepo/dymension/foo_test/tmp/ +RELAYER_DB=/Users/danwt/Documents/dym/d-hyperlane-monorepo/dymension/foo_test/tmp/hyperlane_db_relayer + +# start relayer according to dymension/ethereum_test/commands.sh +export CONFIG_FILES=/Users/danwt/Documents/dym/d-hyperlane-monorepo/dymension/ethereum_test/configs/agent-config.json + +################################# +# DO A TRANSFER ETHEREUM -> HUB + +HUB_RECEIVER_ADDR_NATIVE="dym1yvq7swunxwduq5kkmuftqccxgqk3f6nsaf3sqz" +HUB_RECEIVER_ADDR=$(dymd q forward hl-eth-recipient $HUB_RECEIVER_ADDR_NATIVE) +AMT=5 +DEMO_MEMO="0x68656c6c6f" # 'hello' + +# confirm balance +cast call $COLLAT_TOKEN_CONTRACT_RAW "balanceOf(address)(uint256)" $HYP_ADDR --rpc-url http://localhost:8545 +cast call "0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1" "balanceOf(address)(uint256)" $HYP_ADDR --rpc-url http://localhost:8545 + +cast send $FOO_TOKEN_CONTRACT_RAW "approve(address,uint256)" "$COLLAT_TOKEN_CONTRACT_RAW" 1000000000000000000 --private-key $HYP_KEY --rpc-url http://localhost:8545 + +cast send $COLLAT_TOKEN_CONTRACT_RAW "transferRemote(uint32,bytes32,uint256)" $HUB_DOMAIN $HUB_RECEIVER_ADDR $AMT --private-key $HYP_KEY --rpc-url http://localhost:8545 +cast send $COLLAT_TOKEN_CONTRACT_RAW "transferRemoteMemo(uint32,bytes32,uint256,bytes)" $HUB_DOMAIN $HUB_RECEIVER_ADDR $AMT $DEMO_MEMO --private-key $HYP_KEY --rpc-url http://localhost:8545 + +hyperlane warp send --symbol FOO --amount $AMT --recipient $HUB_RECEIVER_ADDR --private-key $HYP_KEY + +cast call "0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1" "balanceOf(address)(uint256)" $HYP_ADDR --rpc-url http://localhost:8545 + + + + diff --git a/dymension/foo_test/configs/.gitignore b/dymension/foo_test/configs/.gitignore new file mode 100644 index 00000000000..a6e874cfcab --- /dev/null +++ b/dymension/foo_test/configs/.gitignore @@ -0,0 +1 @@ +core-config.yaml diff --git a/dymension/foo_test/configs/warp-route-deployment.yaml b/dymension/foo_test/configs/warp-route-deployment.yaml new file mode 100644 index 00000000000..2a9932ae865 --- /dev/null +++ b/dymension/foo_test/configs/warp-route-deployment.yaml @@ -0,0 +1,18 @@ +anvil0: + interchainSecurityModule: "0x0000000000000000000000000000000000000000" + owner: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + type: collateralMemo + token: "0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1" + decimals: 18 + name: Foo + symbol: FOO + isNft: false + gas: 1 +dymension: + foreignDeployment: "0x726f757465725f61707000000000000000000000000000ff0000000000000000" + mailbox: "0x68797065726c616e650000000000000000000000000000000000000000000000" + owner: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + type: syntheticMemo + token: "0x726f757465725f61707000000000000000000000000000ff0000000000000000" + interchainSecurityModule: "0x0000000000000000000000000000000000000000" + gas: 1 diff --git a/dymension/foo_test/foundry/.github/workflows/test.yml b/dymension/foo_test/foundry/.github/workflows/test.yml new file mode 100644 index 00000000000..34a4a527be6 --- /dev/null +++ b/dymension/foo_test/foundry/.github/workflows/test.yml @@ -0,0 +1,43 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + strategy: + fail-fast: true + + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Show Forge version + run: | + forge --version + + - name: Run Forge fmt + run: | + forge fmt --check + id: fmt + + - name: Run Forge build + run: | + forge build --sizes + id: build + + - name: Run Forge tests + run: | + forge test -vvv + id: test diff --git a/dymension/foo_test/foundry/.gitignore b/dymension/foo_test/foundry/.gitignore new file mode 100644 index 00000000000..85198aaa55b --- /dev/null +++ b/dymension/foo_test/foundry/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/dymension/foo_test/foundry/README.md b/dymension/foo_test/foundry/README.md new file mode 100644 index 00000000000..8817d6ab7b2 --- /dev/null +++ b/dymension/foo_test/foundry/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/dymension/foo_test/foundry/foundry.toml b/dymension/foo_test/foundry/foundry.toml new file mode 100644 index 00000000000..e7a3821aa15 --- /dev/null +++ b/dymension/foo_test/foundry/foundry.toml @@ -0,0 +1,7 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +remappings = ['@openzeppelin=lib/openzeppelin-contracts'] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/dymension/foo_test/foundry/lib/forge-std b/dymension/foo_test/foundry/lib/forge-std new file mode 160000 index 00000000000..77041d2ce69 --- /dev/null +++ b/dymension/foo_test/foundry/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 77041d2ce690e692d6e03cc812b57d1ddaa4d505 diff --git a/dymension/foo_test/foundry/lib/openzeppelin-contracts b/dymension/foo_test/foundry/lib/openzeppelin-contracts new file mode 160000 index 00000000000..e4f70216d75 --- /dev/null +++ b/dymension/foo_test/foundry/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit e4f70216d759d8e6a64144a9e1f7bbeed78e7079 diff --git a/dymension/foo_test/foundry/script/Foo.s.sol b/dymension/foo_test/foundry/script/Foo.s.sol new file mode 100644 index 00000000000..809c2d03e35 --- /dev/null +++ b/dymension/foo_test/foundry/script/Foo.s.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, console} from "forge-std/Script.sol"; +import {Foo} from "../src/Foo.sol"; + +contract DeployFoo is Script { + uint256 public constant INITIAL_SUPPLY = 1000000; + + function run() external { + vm.startBroadcast(); + new Foo(INITIAL_SUPPLY); + vm.stopBroadcast(); + } +} diff --git a/dymension/foo_test/foundry/src/Foo.sol b/dymension/foo_test/foundry/src/Foo.sol new file mode 100644 index 00000000000..f12ee694502 --- /dev/null +++ b/dymension/foo_test/foundry/src/Foo.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract Foo is ERC20 { + constructor(uint256 _initialSupply) ERC20("Foo", "FOO") { + _mint(msg.sender, _initialSupply); + } +} diff --git a/dymension/foo_test/foundry/test/Counter.t.sol b/dymension/foo_test/foundry/test/Counter.t.sol new file mode 100644 index 00000000000..54b724f7ae7 --- /dev/null +++ b/dymension/foo_test/foundry/test/Counter.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console} from "forge-std/Test.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterTest is Test { + Counter public counter; + + function setUp() public { + counter = new Counter(); + counter.setNumber(0); + } + + function test_Increment() public { + counter.increment(); + assertEq(counter.number(), 1); + } + + function testFuzz_SetNumber(uint256 x) public { + counter.setNumber(x); + assertEq(counter.number(), x); + } +} diff --git a/dymension/foo_test/readme.md b/dymension/foo_test/readme.md new file mode 100644 index 00000000000..6da11d6f41e --- /dev/null +++ b/dymension/foo_test/readme.md @@ -0,0 +1,9 @@ +What is this? +A test for Ethereum -> Dymension Hub using an ERC20 token (and synthetic on hub) + + Should result in e.g. + balances: + - amount: "10" + denom: hyperlane/0x726f757465725f61707000000000000000000000000000ff0000000000000000 + pagination: + total: "1" diff --git a/dymension/solana_test/.gitignore b/dymension/solana_test/.gitignore new file mode 100644 index 00000000000..6a570b15652 --- /dev/null +++ b/dymension/solana_test/.gitignore @@ -0,0 +1 @@ +test-ledger/ \ No newline at end of file diff --git a/dymension/solana_test/commands.sh b/dymension/solana_test/commands.sh new file mode 100644 index 00000000000..0edf612fe9d --- /dev/null +++ b/dymension/solana_test/commands.sh @@ -0,0 +1,130 @@ +export BASE_PATH="/Users/danwt/Documents/dym/d-hyperlane-monorepo" + +######################################################################################### +######################################################################################### +# Q: WHAT IS THIS? +# A: It's not a script, but rather some commands, which should be copy pasted as appropriate per the instructions, while in the right directories. +######################################################################################### + +########################### +# STEP: BUILD THE PROGRAMS/CONTRACTS +cd rust/sealevel/programs + +# MUST USE SOLANA v1.14.20 +# (Make sure memo tokens are included in TOKEN_PROGRAM_PATHS in build-programs.sh) +# Build the token programs (.so files) +./build-programs.sh token + +########################### +# STEP: START LOCAL SOLANA INSTANCE + +# first set up some environment variables (needed in every terminal) + +export SOL_ENV_DIR="$BASE_PATH/dymension/solana_test/environments" +export SOL_PROG_DIR="$BASE_PATH/rust/sealevel/target/deploy" +export SOL_KEY_PATH="$BASE_PATH/dymension/solana_test/key.json" +export SOL_CFG_PATH="$HOME/.config/solana/cli/config.yml" +export SOL_ENVIR="local-e2e" +export IGP_PROG_ID="GwHaw8ewMyzZn9vvrZEnTEAAYpLdkGYs195XWcLDCN4U" +export PUB_KEY="2SzyV1kdJNcDYfAqrs5sDFKfHSB6CPrzKhhRb2PyaWre" +export DEPLOYER_PUB_KEY="E9VrvAdGRvCguN2XgXsgu9PNmMM3vZsU8LSUrM68j8ty" +export HUB_DOMAIN=1260813472 +export ETH_DOMAIN=31337 + +# MUST USE SOLANA v2 +solana-test-validator --reset # launch +solana config set --url localhost # adjust client +solana config set --keypair $SOL_KEY_PATH # adjust client + +########################### +# STEP: SETUP INDEXER TO BE ABLE TO OBSERVE MESSAGES +cd rust/main + +## build the indexer +cargo build --release --bin scraper --bin init-db + +# start a db +docker run --rm --name scraper-testnet-postgres -e POSTGRES_PASSWORD=47221c18c610 -p 5432:5432 postgres:14 & + +# in another tab, populate the db with tables etc +HYP_DB="postgresql://postgres:47221c18c610@localhost:5432/postgres" \ +./target/release/init-db + +# start the scraper +# (if it errors, just try again) +RUST_BACKTRACE=full \ +HYP_LOG_LEVEL=debug \ +HYP_LOG_FORMAT=compact \ +HYP_METRICSPORT=9093 \ +HYP_DB="postgresql://postgres:47221c18c610@localhost:5432/postgres" \ +HYP_CHAINSTOSCRAPE="sealeveltest1" \ +./target/release/scraper + +# in another tab, shell into the db to be able to run queries +docker exec -it scraper-testnet-postgres psql -U postgres +\dt # list tables + +########################### +# STEP: DEPLOY CONTRACTS +cd rust/sealevel/client + +# Prelims: +# - need protoc installed locally +# e.g. protoc --version +# libprotoc 29.3 + +# MUST USE SOLANA v1.18.18 + +# core +cargo run -- -k $SOL_KEY_PATH --config $SOL_CFG_PATH \ + core deploy \ + --environment $SOL_ENVIR \ + --environments-dir $SOL_ENV_DIR \ + --built-so-dir $SOL_PROG_DIR \ + --chain sealeveltest1 \ + --local-domain "$ETH_DOMAIN" # todo, solana domain + +# igp, not optional +cargo run -- -k $SOL_KEY_PATH --config $SOL_CFG_PATH \ + igp configure\ + --gas-oracle-config-file $SOL_ENV_DIR/$SOL_ENVIR/gas-oracle-configs.json \ + --chain-config-file $SOL_ENV_DIR/$SOL_ENVIR/chain-config.json \ + --program-id $IGP_PROG_ID \ + --chain sealeveltest1 + +# warp route. This is configured for our token nativeMemo (different than upstream!) +cargo run -- -k $SOL_KEY_PATH --config $SOL_CFG_PATH \ + warp-route deploy \ + --environment $SOL_ENVIR \ + --environments-dir $SOL_ENV_DIR \ + --token-config-file $SOL_ENV_DIR/$SOL_ENVIR/warp-routes/testwarproute/token-config.json \ + --built-so-dir $SOL_PROG_DIR \ + --warp-route-name testwarproute \ + --chain-config-file $SOL_ENV_DIR/$SOL_ENVIR/chain-config.json \ + --ata-payer-funding-amount 1000000000 + +PROGRAM_ID=$(jq -r '.sealeveltest1.base58' $SOL_ENV_DIR/$SOL_ENVIR/warp-routes/testwarproute/program-ids.json) + +########################### +# STEP: TRANSFER + +DUMMY_RECIPIENT="FeSKs7MbwF86PVuofzhKmzWVVFjyVtBTYXJZqQkBYzB6" +EXAMPLE_MEMO="0x0ac7010a087472616e7366657212096368616e6e656c2d301a4b0a446962632f394131454143443533413641313937414443383144463941343946304334413236463746463638354143463431354545373236443744353937393645373141371203313030222a64796d317133303476717239677870766c366b766c656b747238637867743532747879636138347333782a2b6574686d3161333079306839356137703338706c6e76357330326c7a72676379306d3078756d7130796d6e320038a0f2daf1f5c0a89a18122c0a2a64796d31327637353033616664356e7763397030636438766632363464617965646671767a6b657a6c34" +# note: can rederive them memo with this if needed (requires appropriate dymd binary) +# `dymd q forward memo-hl-to-ibc "channel-0" ethm1a30y0h95a7p38plnv5s02lzrgcy0m0xumq0ymn 100ibc/9A1EACD53A6A197ADC81DF9A49F0C4A26F7FF685ACF415EE726D7D59796E71A7 5m dym12v7503afd5nwc9p0cd8vf264dayedfqvzkezl4` + +# initate the transfer, it should result in the message being included with a memo in the outbound messages box +cargo run -- -k $SOL_KEY_PATH --config $SOL_CFG_PATH \ + token transfer-remote-memo \ + --program-id $PROGRAM_ID \ + $SOL_KEY_PATH 100 $HUB_DOMAIN $DUMMY_RECIPIENT native $EXAMPLE_MEMO + +########################### +# STEP: USE INDEXER TO CHECK THE RESULT + +# in the psql tab +select msg_body from message; +# it should have a long body with a 000...00064 part (amt=100) and then a long memo part afterwards + +# sanity check other cli cmds +cargo run -- -k $SOL_KEY_PATH --config $SOL_CFG_PATH token query native-memo diff --git a/dymension/solana_test/environments/local-e2e/accounts/test_deployer-account.json b/dymension/solana_test/environments/local-e2e/accounts/test_deployer-account.json new file mode 100644 index 00000000000..34f58821c9f --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/accounts/test_deployer-account.json @@ -0,0 +1,13 @@ +{ + "pubkey": "E9VrvAdGRvCguN2XgXsgu9PNmMM3vZsU8LSUrM68j8ty", + "account": { + "lamports": 500000000000000000, + "data": [ + "", + "base64" + ], + "owner": "11111111111111111111111111111111", + "executable": false, + "rentEpoch": 0 + } +} diff --git a/dymension/solana_test/environments/local-e2e/accounts/test_deployer-keypair.json b/dymension/solana_test/environments/local-e2e/accounts/test_deployer-keypair.json new file mode 100644 index 00000000000..36e1ec67862 --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/accounts/test_deployer-keypair.json @@ -0,0 +1 @@ +[137,43,246,148,154,244,35,62,98,248,84,203,54,24,188,26,62,227,52,29,199,26,218,8,196,213,222,202,35,154,207,79,195,85,53,151,7,182,83,94,59,5,131,252,40,75,87,11,243,118,71,59,195,222,212,148,179,233,253,121,97,210,114,98] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/chain-config.json b/dymension/solana_test/environments/local-e2e/chain-config.json new file mode 100644 index 00000000000..3fa20441715 --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/chain-config.json @@ -0,0 +1,22 @@ +{ + "sealeveltest1": { + "chainId": 13375, + "isTestnet": true, + "name": "sealeveltest1", + "rpcUrls": [ + { + "http": "http://127.0.0.1:8899" + } + ] + }, + "sealeveltest2": { + "chainId": 13376, + "isTestnet": true, + "name": "sealeveltest2", + "rpcUrls": [ + { + "http": "http://127.0.0.1:8899" + } + ] + } +} \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/gas-oracle-configs.json b/dymension/solana_test/environments/local-e2e/gas-oracle-configs.json new file mode 100644 index 00000000000..896508f1423 --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/gas-oracle-configs.json @@ -0,0 +1,22 @@ +{ + "sealeveltest1": { + "sealeveltest2": { + "oracleConfig": { + "tokenExchangeRate": "1000000000000000000", + "gasPrice": "1", + "tokenDecimals": 9 + }, + "overhead": 100000 + } + }, + "sealeveltest2": { + "sealeveltest1": { + "oracleConfig": { + "tokenExchangeRate": "1000000000000000000", + "gasPrice": "1", + "tokenDecimals": 9 + }, + "overhead": 100000 + } + } +} diff --git a/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_igp-buffer.json b/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_igp-buffer.json new file mode 100644 index 00000000000..015b9174111 --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_igp-buffer.json @@ -0,0 +1 @@ +[237,39,251,35,251,40,175,19,168,134,119,49,14,237,75,252,101,142,177,6,29,109,227,144,189,92,129,194,61,15,97,27,143,203,79,241,45,194,99,159,198,39,13,195,146,9,181,119,27,235,24,185,147,147,127,152,12,51,147,182,36,79,147,124] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_igp-keypair.json b/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_igp-keypair.json new file mode 100644 index 00000000000..7f81d6071be --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_igp-keypair.json @@ -0,0 +1 @@ +[134,213,157,143,154,178,76,237,127,55,106,87,207,201,112,195,174,29,19,170,52,199,100,115,102,42,118,82,225,6,249,137,236,199,107,157,79,247,91,219,151,212,248,27,201,224,51,4,188,80,88,44,193,240,200,144,174,68,126,194,114,54,29,85] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_mailbox-buffer.json b/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_mailbox-buffer.json new file mode 100644 index 00000000000..bb8ec9fa1d2 --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_mailbox-buffer.json @@ -0,0 +1 @@ +[58,196,37,72,220,98,15,62,55,203,89,114,137,101,24,2,2,137,174,215,220,29,97,145,231,25,45,186,225,241,212,241,210,155,79,41,73,80,6,136,205,4,203,255,77,164,164,31,36,45,119,229,217,154,107,204,201,101,244,106,192,83,152,50] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_mailbox-keypair.json b/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_mailbox-keypair.json new file mode 100644 index 00000000000..484e2fb1824 --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_mailbox-keypair.json @@ -0,0 +1 @@ +[113,244,152,170,85,122,42,51,10,74,244,18,91,8,135,77,156,19,172,122,139,50,248,3,186,184,186,140,110,165,78,161,76,88,146,213,185,127,121,92,132,2,249,73,19,192,73,170,105,85,247,241,48,175,67,28,165,29,224,252,173,165,38,140] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_multisig_ism_message_id-buffer.json b/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_multisig_ism_message_id-buffer.json new file mode 100644 index 00000000000..d7bad0abcc0 --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_multisig_ism_message_id-buffer.json @@ -0,0 +1 @@ +[175,209,8,26,128,26,49,96,248,87,112,113,246,98,133,135,215,250,127,52,209,195,236,98,122,121,177,232,69,208,242,110,243,45,195,30,195,98,13,208,210,253,123,154,23,172,203,47,196,213,208,108,211,60,75,14,0,122,133,174,217,65,39,255] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json b/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json new file mode 100644 index 00000000000..243fcbd9ad4 --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json @@ -0,0 +1 @@ +[135,153,145,193,50,88,169,205,206,171,48,1,17,242,3,43,225,72,101,163,93,126,105,165,159,44,243,196,182,240,4,87,22,253,47,198,217,75,23,60,181,129,251,103,140,170,111,35,152,97,16,23,64,17,198,239,79,225,120,141,55,38,60,86] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_validator_announce-buffer.json b/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_validator_announce-buffer.json new file mode 100644 index 00000000000..f18e78806c2 --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_validator_announce-buffer.json @@ -0,0 +1 @@ +[233,19,223,77,30,238,100,39,159,37,179,78,5,234,42,13,168,159,114,70,138,190,182,112,37,155,20,147,138,42,187,57,6,55,223,133,153,35,253,23,250,201,23,205,63,126,244,157,226,216,123,162,196,34,10,11,165,34,160,60,197,90,39,123] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_validator_announce-keypair.json b/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_validator_announce-keypair.json new file mode 100644 index 00000000000..3428c9c4978 --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_validator_announce-keypair.json @@ -0,0 +1 @@ +[252,76,67,201,250,68,86,32,216,136,163,46,192,20,249,175,209,94,101,235,24,240,204,4,246,159,180,138,253,20,48,146,182,104,250,124,231,168,239,248,95,199,219,250,126,156,57,113,83,209,232,171,10,90,153,238,72,138,186,34,77,87,172,211] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/sealeveltest1/core/program-ids.json b/dymension/solana_test/environments/local-e2e/sealeveltest1/core/program-ids.json new file mode 100644 index 00000000000..a509b7def5c --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/sealeveltest1/core/program-ids.json @@ -0,0 +1,8 @@ +{ + "mailbox": "692KZJaoe2KRcD6uhCQDLLXnLNA5ZLnfvdqjE4aX9iu1", + "validator_announce": "DH43ae1LwemXAboWwSh8zc9pG8j72gKUEXNi57w8fEnn", + "multisig_ism_message_id": "2YjtZDiUoptoSsA5eVrDCcX6wxNK6YoEVW7y82x5Z2fw", + "igp_program_id": "GwHaw8ewMyzZn9vvrZEnTEAAYpLdkGYs195XWcLDCN4U", + "overhead_igp_account": "EBEZGxTABcfHgPH1vZZc9BnFWHjne4nzqApZZxGTCgsn", + "igp_account": "DrFtxirPPsfdY4HQiNZj2A9o4Ux7JaL3gELANgAoihhp" +} \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_igp-buffer.json b/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_igp-buffer.json new file mode 100644 index 00000000000..2dc45e7f9f9 --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_igp-buffer.json @@ -0,0 +1 @@ +[212,84,48,41,63,5,10,95,124,16,158,109,239,222,24,244,97,76,68,17,226,233,224,117,139,110,2,105,210,5,196,188,52,179,160,58,115,173,225,13,223,95,68,35,66,218,240,210,171,245,133,205,39,8,230,21,152,182,143,119,57,146,101,197] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_igp-keypair.json b/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_igp-keypair.json new file mode 100644 index 00000000000..9ff781edaa3 --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_igp-keypair.json @@ -0,0 +1 @@ +[86,251,114,232,180,241,250,134,177,246,42,46,186,189,49,90,89,204,100,125,155,25,147,101,214,199,217,85,206,213,35,196,210,137,243,172,88,191,74,240,37,103,187,105,166,208,57,17,183,226,207,228,64,84,242,235,243,87,113,31,26,49,57,214] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_mailbox-buffer.json b/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_mailbox-buffer.json new file mode 100644 index 00000000000..5e6901688d5 --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_mailbox-buffer.json @@ -0,0 +1 @@ +[5,14,180,169,228,232,44,51,156,194,216,43,21,7,156,36,97,64,153,176,203,229,158,48,113,164,193,32,241,162,153,250,25,216,99,19,225,30,177,181,86,171,118,189,242,153,93,2,58,37,116,225,128,36,238,213,208,184,0,250,40,102,220,220] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_mailbox-keypair.json b/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_mailbox-keypair.json new file mode 100644 index 00000000000..5d17fc11241 --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_mailbox-keypair.json @@ -0,0 +1 @@ +[222,255,22,134,94,105,56,122,208,160,175,127,249,7,9,61,12,110,163,100,255,114,133,62,171,49,222,193,39,231,136,249,131,251,23,52,193,139,17,122,124,153,164,193,162,233,9,87,24,40,187,244,129,39,39,8,62,191,198,138,8,53,251,70] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_multisig_ism_message_id-buffer.json b/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_multisig_ism_message_id-buffer.json new file mode 100644 index 00000000000..c081266c295 --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_multisig_ism_message_id-buffer.json @@ -0,0 +1 @@ +[226,8,65,198,226,234,141,166,249,42,180,21,182,83,44,3,157,63,76,184,30,73,124,26,50,244,78,167,163,147,81,220,182,153,58,64,105,155,228,212,203,141,62,180,188,230,43,4,170,95,105,88,74,37,87,224,246,112,64,227,206,221,87,16] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json b/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json new file mode 100644 index 00000000000..ee3021ce31f --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json @@ -0,0 +1 @@ +[119,156,99,147,184,17,168,119,174,254,129,203,223,83,51,35,99,182,15,15,58,156,141,241,245,169,72,101,143,147,15,8,50,213,208,201,12,135,40,197,122,87,89,157,235,179,146,95,204,46,89,252,205,83,58,61,205,33,83,138,54,25,23,176] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_validator_announce-buffer.json b/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_validator_announce-buffer.json new file mode 100644 index 00000000000..ea07f0b5b20 --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_validator_announce-buffer.json @@ -0,0 +1 @@ +[22,9,199,128,86,231,188,193,17,243,130,150,163,211,162,193,69,94,52,176,251,175,47,98,195,98,61,158,133,177,103,236,112,38,3,254,110,66,121,250,138,34,193,44,212,54,124,136,9,218,87,42,98,48,71,97,168,152,145,101,78,92,21,78] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_validator_announce-keypair.json b/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_validator_announce-keypair.json new file mode 100644 index 00000000000..e15794f2715 --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_validator_announce-keypair.json @@ -0,0 +1 @@ +[189,70,127,148,71,67,60,187,176,28,129,66,59,243,228,106,194,143,14,238,72,50,253,210,16,179,234,160,154,50,131,9,36,214,41,0,180,6,245,233,254,165,69,146,82,0,244,49,220,233,60,78,63,235,177,253,34,149,232,214,43,170,124,38] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/sealeveltest2/core/program-ids.json b/dymension/solana_test/environments/local-e2e/sealeveltest2/core/program-ids.json new file mode 100644 index 00000000000..2df823c55cb --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/sealeveltest2/core/program-ids.json @@ -0,0 +1,8 @@ +{ + "mailbox": "9tCUWNjpqcf3NUSrtp7vquYVCwbEByvLjZUrhG5dgvhj", + "validator_announce": "3Uo5j2Bti9aZtrDqJmAyuwiFaJFPFoNL5yxTpVCNcUhb", + "multisig_ism_message_id": "4RSV6iyqW9X66Xq3RDCVsKJ7hMba5uv6XP8ttgxjVUB1", + "igp_program_id": "FArd4tEikwz2fk3MB7S9kC82NGhkgT6f9aXi3C5cw1E5", + "overhead_igp_account": "91sHQw73sEeio7BtU8cgrXxgTNhJ1nbBsbjHGD6cTNoJ", + "igp_account": "G5rGigZBL8NmxCaukK2CAKr9Jq4SUfAhsjzeri7GUraK" +} \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/.gitignore b/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/.gitignore new file mode 100644 index 00000000000..809630cf069 --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/.gitignore @@ -0,0 +1,2 @@ +# want to generate these +program-ids.json \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token-sealeveltest2-buffer.json b/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token-sealeveltest2-buffer.json new file mode 100644 index 00000000000..0f3e16da3db --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token-sealeveltest2-buffer.json @@ -0,0 +1 @@ +[197,130,38,209,204,144,194,29,110,24,216,248,114,197,109,232,116,177,190,244,112,202,255,252,1,42,180,0,147,105,29,255,17,236,106,217,223,80,66,200,131,176,62,209,68,39,218,207,136,65,182,70,1,84,219,11,149,73,70,7,27,225,32,134] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token-sealeveltest2-keypair.json b/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token-sealeveltest2-keypair.json new file mode 100644 index 00000000000..5bd74ba3722 --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token-sealeveltest2-keypair.json @@ -0,0 +1 @@ +[150,184,67,120,68,103,211,31,174,166,80,126,38,201,166,221,187,186,138,146,47,236,182,38,17,22,202,64,143,124,229,151,35,23,249,97,93,78,188,36,25,173,75,136,88,14,42,128,160,59,44,122,96,188,150,13,231,214,147,77,188,55,168,126] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native-sealeveltest1-buffer.json b/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native-sealeveltest1-buffer.json new file mode 100644 index 00000000000..182cbb2439d --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native-sealeveltest1-buffer.json @@ -0,0 +1 @@ +[251,213,214,65,188,22,79,45,246,127,47,30,86,147,53,54,127,219,125,170,97,68,59,28,245,79,23,255,19,157,178,173,226,130,171,187,74,79,38,186,92,215,56,60,127,33,211,239,238,120,112,110,143,219,189,65,67,167,211,218,118,36,32,234] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native-sealeveltest1-keypair.json b/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native-sealeveltest1-keypair.json new file mode 100644 index 00000000000..676bed53f72 --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native-sealeveltest1-keypair.json @@ -0,0 +1 @@ +[245,11,246,177,71,74,101,17,124,83,118,145,35,212,169,255,215,255,1,94,185,33,54,171,79,86,221,83,104,234,22,42,167,123,78,46,210,49,137,76,200,203,142,238,33,173,204,112,93,132,137,188,204,107,47,207,64,163,88,222,35,230,11,123] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native_memo-sealeveltest1-buffer.json b/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native_memo-sealeveltest1-buffer.json new file mode 100644 index 00000000000..860de2fe30d --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native_memo-sealeveltest1-buffer.json @@ -0,0 +1 @@ +[156,151,45,113,86,8,217,27,91,175,186,118,15,182,99,95,86,143,121,120,7,5,121,8,50,17,4,80,5,178,103,144,97,73,19,63,116,26,248,217,180,157,40,206,38,22,36,216,179,78,165,211,159,161,231,85,152,107,224,161,253,183,168,179] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native_memo-sealeveltest1-keypair.json b/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native_memo-sealeveltest1-keypair.json new file mode 100644 index 00000000000..e90d369461a --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native_memo-sealeveltest1-keypair.json @@ -0,0 +1 @@ +[17,87,92,157,39,157,157,1,147,214,172,127,152,181,114,70,100,106,147,40,77,214,113,124,128,154,142,33,22,16,254,107,248,190,59,196,60,230,124,45,105,212,210,200,154,28,117,138,241,39,185,231,47,2,214,132,142,120,57,228,206,189,64,204] \ No newline at end of file diff --git a/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/token-config.json b/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/token-config.json new file mode 100644 index 00000000000..ea37cca7aea --- /dev/null +++ b/dymension/solana_test/environments/local-e2e/warp-routes/testwarproute/token-config.json @@ -0,0 +1,12 @@ +{ + "sealeveltest1": { + "type": "nativeMemo", + "decimals": 9 + }, + "sealeveltest2": { + "type": "synthetic", + "decimals": 9, + "name": "Solana", + "symbol": "SOL" + } +} \ No newline at end of file diff --git a/dymension/solana_test/key.json b/dymension/solana_test/key.json new file mode 100644 index 00000000000..0cb526d15e3 --- /dev/null +++ b/dymension/solana_test/key.json @@ -0,0 +1 @@ +[7,43,157,20,5,71,179,108,120,72,181,64,167,47,224,55,23,234,68,103,249,188,2,227,150,102,192,184,130,241,215,247,21,132,194,37,186,203,67,214,141,144,83,185,27,60,19,84,216,212,254,115,156,182,235,156,70,114,22,166,221,41,209,251] \ No newline at end of file diff --git a/dymension/solana_test/readme.md b/dymension/solana_test/readme.md new file mode 100644 index 00000000000..f4f16631d71 --- /dev/null +++ b/dymension/solana_test/readme.md @@ -0,0 +1,7 @@ +What is this? + +It's a test to check from the user perspective that: + +1. Deploying the memo program works +2. Transferring and including a memo works +3. Memo is correctly included in the outbound message diff --git a/dymension/solana_test/scraper.json b/dymension/solana_test/scraper.json new file mode 100644 index 00000000000..b7520f7ac4a --- /dev/null +++ b/dymension/solana_test/scraper.json @@ -0,0 +1,28 @@ +{ + "metricsPort": 9093, + "chains": { + "sealeveltest1": { + "domain": 13375, + "connection": { + "type": "solana", + "urls": [ + ] + }, + "index": { + }, + "addresses": { + "mailbox": "0x77d43342b39f48f997dd7ad41df004f56f4c43d782b7872f69968484c7767ef3", + "interchainGasPaymaster": "0x556a615e0ce375b627c626d7439a2688a14d37f37bf88f10385fd9d9921e60a1", + "validatorAnnounce": "0x651268d47874bd59d9b15a7d40e7259c67ac9b64e71454d3f8554660d0d9693c", + "merkleTreeHook": "0x6f01e5a23c9e7138658371a691e634654107b4094136d1a6f1d71880f512e378" + } + } + }, + "chainsToScrape": [ + "sealeveltest1" + ], + "tracing": { + "level": "info", + "fmt": "pretty" + } +} \ No newline at end of file diff --git a/dymension/solana_test2/.gitignore b/dymension/solana_test2/.gitignore new file mode 100644 index 00000000000..6a570b15652 --- /dev/null +++ b/dymension/solana_test2/.gitignore @@ -0,0 +1 @@ +test-ledger/ \ No newline at end of file diff --git a/dymension/solana_test2/commands.sh b/dymension/solana_test2/commands.sh new file mode 100644 index 00000000000..d899ec8b906 --- /dev/null +++ b/dymension/solana_test2/commands.sh @@ -0,0 +1,130 @@ +export BASE_PATH="/Users/danwt/Documents/dym/d-hyperlane-monorepo" + +######################################################################################### +######################################################################################### +# Q: WHAT IS THIS? +# A: SAME AS solana_test but it uses synthetic memo instead of native memo for testing +######################################################################################### + +########################### +# STEP: BUILD THE PROGRAMS/CONTRACTS +cd rust/sealevel/programs + +# MUST USE SOLANA v1.14.20 +# (Make sure memo tokens are included in TOKEN_PROGRAM_PATHS in build-programs.sh) +# Build the token programs (.so files) +./build-programs.sh token + +########################### +# STEP: START LOCAL SOLANA INSTANCE + +# first set up some environment variables (needed in every terminal) + +export SOL_ENV_DIR="$BASE_PATH/dymension/solana_test2/environments" +export SOL_PROG_DIR="$BASE_PATH/rust/sealevel/target/deploy" +export SOL_KEY_PATH="$BASE_PATH/dymension/solana_test2/key.json" +export SOL_CFG_PATH="$HOME/.config/solana/cli/config.yml" +export SOL_ENVIR="local-e2e" +export IGP_PROG_ID="GwHaw8ewMyzZn9vvrZEnTEAAYpLdkGYs195XWcLDCN4U" +export PUB_KEY="2SzyV1kdJNcDYfAqrs5sDFKfHSB6CPrzKhhRb2PyaWre" +export DEPLOYER_PUB_KEY="E9VrvAdGRvCguN2XgXsgu9PNmMM3vZsU8LSUrM68j8ty" +export HUB_DOMAIN=1260813472 +export ETH_DOMAIN=31337 # solana is 1399811149 https://docs.hyperlane.xyz/docs/reference/domains + +# MUST USE SOLANA v2 +solana-test-validator --reset # launch +solana config set --url localhost # adjust client +solana config set --keypair $SOL_KEY_PATH # adjust client + +########################### +# STEP: SETUP INDEXER TO BE ABLE TO OBSERVE MESSAGES +cd rust/main + +## build the indexer +cargo build --release --bin scraper --bin init-db + +# start a db +docker run --rm --name scraper-testnet-postgres -e POSTGRES_PASSWORD=47221c18c610 -p 5432:5432 postgres:14 & + +# in another tab, populate the db with tables etc +HYP_DB="postgresql://postgres:47221c18c610@localhost:5432/postgres" \ +./target/release/init-db + +# start the scraper +# (if it errors, just try again) +RUST_BACKTRACE=full \ +HYP_LOG_LEVEL=debug \ +HYP_LOG_FORMAT=compact \ +HYP_METRICSPORT=9093 \ +HYP_DB="postgresql://postgres:47221c18c610@localhost:5432/postgres" \ +HYP_CHAINSTOSCRAPE="sealeveltest1" \ +./target/release/scraper + +# in another tab, shell into the db to be able to run queries +docker exec -it scraper-testnet-postgres psql -U postgres +\dt # list tables for debugging/checking + +########################### +# STEP: DEPLOY CONTRACTS +cd rust/sealevel/client + +# Prelims: +# - need protoc installed locally +# e.g. protoc --version +# libprotoc 29.3 + +# MUST USE SOLANA v1.18.18 + +# core +cargo run -- -k $SOL_KEY_PATH --config $SOL_CFG_PATH \ + core deploy \ + --environment $SOL_ENVIR \ + --environments-dir $SOL_ENV_DIR \ + --built-so-dir $SOL_PROG_DIR \ + --chain sealeveltest1 \ + --local-domain "$ETH_DOMAIN" # todo, solana domain + +# igp, not optional +cargo run -- -k $SOL_KEY_PATH --config $SOL_CFG_PATH \ + igp configure\ + --gas-oracle-config-file $SOL_ENV_DIR/$SOL_ENVIR/gas-oracle-configs.json \ + --chain-config-file $SOL_ENV_DIR/$SOL_ENVIR/chain-config.json \ + --program-id $IGP_PROG_ID \ + --chain sealeveltest1 + +# warp route. This is configured for our token synthetic memo (different than upstream!) +cargo run -- -k $SOL_KEY_PATH --config $SOL_CFG_PATH \ + warp-route deploy \ + --environment $SOL_ENVIR \ + --environments-dir $SOL_ENV_DIR \ + --token-config-file $SOL_ENV_DIR/$SOL_ENVIR/warp-routes/testwarproute/token-config.json \ + --built-so-dir $SOL_PROG_DIR \ + --warp-route-name testwarproute \ + --chain-config-file $SOL_ENV_DIR/$SOL_ENVIR/chain-config.json \ + --ata-payer-funding-amount 1000000000 + +PROGRAM_ID=$(jq -r '.sealeveltest1.base58' $SOL_ENV_DIR/$SOL_ENVIR/warp-routes/testwarproute/program-ids.json) + +########################### +# STEP: TRANSFER + +DUMMY_RECIPIENT="FeSKs7MbwF86PVuofzhKmzWVVFjyVtBTYXJZqQkBYzB6" +EXAMPLE_MEMO="0x0ac7010a087472616e7366657212096368616e6e656c2d301a4b0a446962632f394131454143443533413641313937414443383144463941343946304334413236463746463638354143463431354545373236443744353937393645373141371203313030222a64796d317133303476717239677870766c366b766c656b747238637867743532747879636138347333782a2b6574686d3161333079306839356137703338706c6e76357330326c7a72676379306d3078756d7130796d6e320038a0f2daf1f5c0a89a18122c0a2a64796d31327637353033616664356e7763397030636438766632363464617965646671767a6b657a6c34" +# note: can rederive them memo with this if needed (requires appropriate dymd binary) +# `dymd q forward memo-hl-to-ibc "channel-0" ethm1a30y0h95a7p38plnv5s02lzrgcy0m0xumq0ymn 100ibc/9A1EACD53A6A197ADC81DF9A49F0C4A26F7FF685ACF415EE726D7D59796E71A7 5m dym12v7503afd5nwc9p0cd8vf264dayedfqvzkezl4` + +# initate the transfer, it should result in the message being included with a memo in the outbound messages box +cargo run -- -k $SOL_KEY_PATH --config $SOL_CFG_PATH \ + token transfer-remote-memo \ + --program-id $PROGRAM_ID \ + $SOL_KEY_PATH 100 $HUB_DOMAIN $DUMMY_RECIPIENT native $EXAMPLE_MEMO + +########################### +# STEP: USE INDEXER TO CHECK THE RESULT + +# in the psql tab +select msg_body from message; +# it should have a long body with a 000...00064 part (amt=100) and then a long memo part afterwards + +# sanity check other cli cmds +cargo run -- -k $SOL_KEY_PATH --config $SOL_CFG_PATH token query synthetic-memo diff --git a/dymension/solana_test2/environments/local-e2e/accounts/test_deployer-account.json b/dymension/solana_test2/environments/local-e2e/accounts/test_deployer-account.json new file mode 100644 index 00000000000..34f58821c9f --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/accounts/test_deployer-account.json @@ -0,0 +1,13 @@ +{ + "pubkey": "E9VrvAdGRvCguN2XgXsgu9PNmMM3vZsU8LSUrM68j8ty", + "account": { + "lamports": 500000000000000000, + "data": [ + "", + "base64" + ], + "owner": "11111111111111111111111111111111", + "executable": false, + "rentEpoch": 0 + } +} diff --git a/dymension/solana_test2/environments/local-e2e/accounts/test_deployer-keypair.json b/dymension/solana_test2/environments/local-e2e/accounts/test_deployer-keypair.json new file mode 100644 index 00000000000..36e1ec67862 --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/accounts/test_deployer-keypair.json @@ -0,0 +1 @@ +[137,43,246,148,154,244,35,62,98,248,84,203,54,24,188,26,62,227,52,29,199,26,218,8,196,213,222,202,35,154,207,79,195,85,53,151,7,182,83,94,59,5,131,252,40,75,87,11,243,118,71,59,195,222,212,148,179,233,253,121,97,210,114,98] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/chain-config.json b/dymension/solana_test2/environments/local-e2e/chain-config.json new file mode 100644 index 00000000000..3fa20441715 --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/chain-config.json @@ -0,0 +1,22 @@ +{ + "sealeveltest1": { + "chainId": 13375, + "isTestnet": true, + "name": "sealeveltest1", + "rpcUrls": [ + { + "http": "http://127.0.0.1:8899" + } + ] + }, + "sealeveltest2": { + "chainId": 13376, + "isTestnet": true, + "name": "sealeveltest2", + "rpcUrls": [ + { + "http": "http://127.0.0.1:8899" + } + ] + } +} \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/gas-oracle-configs.json b/dymension/solana_test2/environments/local-e2e/gas-oracle-configs.json new file mode 100644 index 00000000000..896508f1423 --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/gas-oracle-configs.json @@ -0,0 +1,22 @@ +{ + "sealeveltest1": { + "sealeveltest2": { + "oracleConfig": { + "tokenExchangeRate": "1000000000000000000", + "gasPrice": "1", + "tokenDecimals": 9 + }, + "overhead": 100000 + } + }, + "sealeveltest2": { + "sealeveltest1": { + "oracleConfig": { + "tokenExchangeRate": "1000000000000000000", + "gasPrice": "1", + "tokenDecimals": 9 + }, + "overhead": 100000 + } + } +} diff --git a/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_igp-buffer.json b/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_igp-buffer.json new file mode 100644 index 00000000000..015b9174111 --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_igp-buffer.json @@ -0,0 +1 @@ +[237,39,251,35,251,40,175,19,168,134,119,49,14,237,75,252,101,142,177,6,29,109,227,144,189,92,129,194,61,15,97,27,143,203,79,241,45,194,99,159,198,39,13,195,146,9,181,119,27,235,24,185,147,147,127,152,12,51,147,182,36,79,147,124] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_igp-keypair.json b/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_igp-keypair.json new file mode 100644 index 00000000000..7f81d6071be --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_igp-keypair.json @@ -0,0 +1 @@ +[134,213,157,143,154,178,76,237,127,55,106,87,207,201,112,195,174,29,19,170,52,199,100,115,102,42,118,82,225,6,249,137,236,199,107,157,79,247,91,219,151,212,248,27,201,224,51,4,188,80,88,44,193,240,200,144,174,68,126,194,114,54,29,85] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_mailbox-buffer.json b/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_mailbox-buffer.json new file mode 100644 index 00000000000..bb8ec9fa1d2 --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_mailbox-buffer.json @@ -0,0 +1 @@ +[58,196,37,72,220,98,15,62,55,203,89,114,137,101,24,2,2,137,174,215,220,29,97,145,231,25,45,186,225,241,212,241,210,155,79,41,73,80,6,136,205,4,203,255,77,164,164,31,36,45,119,229,217,154,107,204,201,101,244,106,192,83,152,50] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_mailbox-keypair.json b/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_mailbox-keypair.json new file mode 100644 index 00000000000..484e2fb1824 --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_mailbox-keypair.json @@ -0,0 +1 @@ +[113,244,152,170,85,122,42,51,10,74,244,18,91,8,135,77,156,19,172,122,139,50,248,3,186,184,186,140,110,165,78,161,76,88,146,213,185,127,121,92,132,2,249,73,19,192,73,170,105,85,247,241,48,175,67,28,165,29,224,252,173,165,38,140] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_multisig_ism_message_id-buffer.json b/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_multisig_ism_message_id-buffer.json new file mode 100644 index 00000000000..d7bad0abcc0 --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_multisig_ism_message_id-buffer.json @@ -0,0 +1 @@ +[175,209,8,26,128,26,49,96,248,87,112,113,246,98,133,135,215,250,127,52,209,195,236,98,122,121,177,232,69,208,242,110,243,45,195,30,195,98,13,208,210,253,123,154,23,172,203,47,196,213,208,108,211,60,75,14,0,122,133,174,217,65,39,255] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json b/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json new file mode 100644 index 00000000000..243fcbd9ad4 --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json @@ -0,0 +1 @@ +[135,153,145,193,50,88,169,205,206,171,48,1,17,242,3,43,225,72,101,163,93,126,105,165,159,44,243,196,182,240,4,87,22,253,47,198,217,75,23,60,181,129,251,103,140,170,111,35,152,97,16,23,64,17,198,239,79,225,120,141,55,38,60,86] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_validator_announce-buffer.json b/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_validator_announce-buffer.json new file mode 100644 index 00000000000..f18e78806c2 --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_validator_announce-buffer.json @@ -0,0 +1 @@ +[233,19,223,77,30,238,100,39,159,37,179,78,5,234,42,13,168,159,114,70,138,190,182,112,37,155,20,147,138,42,187,57,6,55,223,133,153,35,253,23,250,201,23,205,63,126,244,157,226,216,123,162,196,34,10,11,165,34,160,60,197,90,39,123] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_validator_announce-keypair.json b/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_validator_announce-keypair.json new file mode 100644 index 00000000000..3428c9c4978 --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_validator_announce-keypair.json @@ -0,0 +1 @@ +[252,76,67,201,250,68,86,32,216,136,163,46,192,20,249,175,209,94,101,235,24,240,204,4,246,159,180,138,253,20,48,146,182,104,250,124,231,168,239,248,95,199,219,250,126,156,57,113,83,209,232,171,10,90,153,238,72,138,186,34,77,87,172,211] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/program-ids.json b/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/program-ids.json new file mode 100644 index 00000000000..a509b7def5c --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/sealeveltest1/core/program-ids.json @@ -0,0 +1,8 @@ +{ + "mailbox": "692KZJaoe2KRcD6uhCQDLLXnLNA5ZLnfvdqjE4aX9iu1", + "validator_announce": "DH43ae1LwemXAboWwSh8zc9pG8j72gKUEXNi57w8fEnn", + "multisig_ism_message_id": "2YjtZDiUoptoSsA5eVrDCcX6wxNK6YoEVW7y82x5Z2fw", + "igp_program_id": "GwHaw8ewMyzZn9vvrZEnTEAAYpLdkGYs195XWcLDCN4U", + "overhead_igp_account": "EBEZGxTABcfHgPH1vZZc9BnFWHjne4nzqApZZxGTCgsn", + "igp_account": "DrFtxirPPsfdY4HQiNZj2A9o4Ux7JaL3gELANgAoihhp" +} \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_igp-buffer.json b/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_igp-buffer.json new file mode 100644 index 00000000000..2dc45e7f9f9 --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_igp-buffer.json @@ -0,0 +1 @@ +[212,84,48,41,63,5,10,95,124,16,158,109,239,222,24,244,97,76,68,17,226,233,224,117,139,110,2,105,210,5,196,188,52,179,160,58,115,173,225,13,223,95,68,35,66,218,240,210,171,245,133,205,39,8,230,21,152,182,143,119,57,146,101,197] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_igp-keypair.json b/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_igp-keypair.json new file mode 100644 index 00000000000..9ff781edaa3 --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_igp-keypair.json @@ -0,0 +1 @@ +[86,251,114,232,180,241,250,134,177,246,42,46,186,189,49,90,89,204,100,125,155,25,147,101,214,199,217,85,206,213,35,196,210,137,243,172,88,191,74,240,37,103,187,105,166,208,57,17,183,226,207,228,64,84,242,235,243,87,113,31,26,49,57,214] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_mailbox-buffer.json b/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_mailbox-buffer.json new file mode 100644 index 00000000000..5e6901688d5 --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_mailbox-buffer.json @@ -0,0 +1 @@ +[5,14,180,169,228,232,44,51,156,194,216,43,21,7,156,36,97,64,153,176,203,229,158,48,113,164,193,32,241,162,153,250,25,216,99,19,225,30,177,181,86,171,118,189,242,153,93,2,58,37,116,225,128,36,238,213,208,184,0,250,40,102,220,220] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_mailbox-keypair.json b/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_mailbox-keypair.json new file mode 100644 index 00000000000..5d17fc11241 --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_mailbox-keypair.json @@ -0,0 +1 @@ +[222,255,22,134,94,105,56,122,208,160,175,127,249,7,9,61,12,110,163,100,255,114,133,62,171,49,222,193,39,231,136,249,131,251,23,52,193,139,17,122,124,153,164,193,162,233,9,87,24,40,187,244,129,39,39,8,62,191,198,138,8,53,251,70] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_multisig_ism_message_id-buffer.json b/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_multisig_ism_message_id-buffer.json new file mode 100644 index 00000000000..c081266c295 --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_multisig_ism_message_id-buffer.json @@ -0,0 +1 @@ +[226,8,65,198,226,234,141,166,249,42,180,21,182,83,44,3,157,63,76,184,30,73,124,26,50,244,78,167,163,147,81,220,182,153,58,64,105,155,228,212,203,141,62,180,188,230,43,4,170,95,105,88,74,37,87,224,246,112,64,227,206,221,87,16] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json b/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json new file mode 100644 index 00000000000..ee3021ce31f --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json @@ -0,0 +1 @@ +[119,156,99,147,184,17,168,119,174,254,129,203,223,83,51,35,99,182,15,15,58,156,141,241,245,169,72,101,143,147,15,8,50,213,208,201,12,135,40,197,122,87,89,157,235,179,146,95,204,46,89,252,205,83,58,61,205,33,83,138,54,25,23,176] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_validator_announce-buffer.json b/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_validator_announce-buffer.json new file mode 100644 index 00000000000..ea07f0b5b20 --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_validator_announce-buffer.json @@ -0,0 +1 @@ +[22,9,199,128,86,231,188,193,17,243,130,150,163,211,162,193,69,94,52,176,251,175,47,98,195,98,61,158,133,177,103,236,112,38,3,254,110,66,121,250,138,34,193,44,212,54,124,136,9,218,87,42,98,48,71,97,168,152,145,101,78,92,21,78] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_validator_announce-keypair.json b/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_validator_announce-keypair.json new file mode 100644 index 00000000000..e15794f2715 --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_validator_announce-keypair.json @@ -0,0 +1 @@ +[189,70,127,148,71,67,60,187,176,28,129,66,59,243,228,106,194,143,14,238,72,50,253,210,16,179,234,160,154,50,131,9,36,214,41,0,180,6,245,233,254,165,69,146,82,0,244,49,220,233,60,78,63,235,177,253,34,149,232,214,43,170,124,38] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/program-ids.json b/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/program-ids.json new file mode 100644 index 00000000000..2df823c55cb --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/sealeveltest2/core/program-ids.json @@ -0,0 +1,8 @@ +{ + "mailbox": "9tCUWNjpqcf3NUSrtp7vquYVCwbEByvLjZUrhG5dgvhj", + "validator_announce": "3Uo5j2Bti9aZtrDqJmAyuwiFaJFPFoNL5yxTpVCNcUhb", + "multisig_ism_message_id": "4RSV6iyqW9X66Xq3RDCVsKJ7hMba5uv6XP8ttgxjVUB1", + "igp_program_id": "FArd4tEikwz2fk3MB7S9kC82NGhkgT6f9aXi3C5cw1E5", + "overhead_igp_account": "91sHQw73sEeio7BtU8cgrXxgTNhJ1nbBsbjHGD6cTNoJ", + "igp_account": "G5rGigZBL8NmxCaukK2CAKr9Jq4SUfAhsjzeri7GUraK" +} \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/.gitignore b/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/.gitignore new file mode 100644 index 00000000000..809630cf069 --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/.gitignore @@ -0,0 +1,2 @@ +# want to generate these +program-ids.json \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token-sealeveltest2-buffer.json b/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token-sealeveltest2-buffer.json new file mode 100644 index 00000000000..0f3e16da3db --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token-sealeveltest2-buffer.json @@ -0,0 +1 @@ +[197,130,38,209,204,144,194,29,110,24,216,248,114,197,109,232,116,177,190,244,112,202,255,252,1,42,180,0,147,105,29,255,17,236,106,217,223,80,66,200,131,176,62,209,68,39,218,207,136,65,182,70,1,84,219,11,149,73,70,7,27,225,32,134] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token-sealeveltest2-keypair.json b/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token-sealeveltest2-keypair.json new file mode 100644 index 00000000000..5bd74ba3722 --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token-sealeveltest2-keypair.json @@ -0,0 +1 @@ +[150,184,67,120,68,103,211,31,174,166,80,126,38,201,166,221,187,186,138,146,47,236,182,38,17,22,202,64,143,124,229,151,35,23,249,97,93,78,188,36,25,173,75,136,88,14,42,128,160,59,44,122,96,188,150,13,231,214,147,77,188,55,168,126] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native-sealeveltest1-buffer.json b/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native-sealeveltest1-buffer.json new file mode 100644 index 00000000000..182cbb2439d --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native-sealeveltest1-buffer.json @@ -0,0 +1 @@ +[251,213,214,65,188,22,79,45,246,127,47,30,86,147,53,54,127,219,125,170,97,68,59,28,245,79,23,255,19,157,178,173,226,130,171,187,74,79,38,186,92,215,56,60,127,33,211,239,238,120,112,110,143,219,189,65,67,167,211,218,118,36,32,234] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native-sealeveltest1-keypair.json b/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native-sealeveltest1-keypair.json new file mode 100644 index 00000000000..676bed53f72 --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native-sealeveltest1-keypair.json @@ -0,0 +1 @@ +[245,11,246,177,71,74,101,17,124,83,118,145,35,212,169,255,215,255,1,94,185,33,54,171,79,86,221,83,104,234,22,42,167,123,78,46,210,49,137,76,200,203,142,238,33,173,204,112,93,132,137,188,204,107,47,207,64,163,88,222,35,230,11,123] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native_memo-sealeveltest1-buffer.json b/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native_memo-sealeveltest1-buffer.json new file mode 100644 index 00000000000..860de2fe30d --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native_memo-sealeveltest1-buffer.json @@ -0,0 +1 @@ +[156,151,45,113,86,8,217,27,91,175,186,118,15,182,99,95,86,143,121,120,7,5,121,8,50,17,4,80,5,178,103,144,97,73,19,63,116,26,248,217,180,157,40,206,38,22,36,216,179,78,165,211,159,161,231,85,152,107,224,161,253,183,168,179] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native_memo-sealeveltest1-keypair.json b/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native_memo-sealeveltest1-keypair.json new file mode 100644 index 00000000000..e90d369461a --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native_memo-sealeveltest1-keypair.json @@ -0,0 +1 @@ +[17,87,92,157,39,157,157,1,147,214,172,127,152,181,114,70,100,106,147,40,77,214,113,124,128,154,142,33,22,16,254,107,248,190,59,196,60,230,124,45,105,212,210,200,154,28,117,138,241,39,185,231,47,2,214,132,142,120,57,228,206,189,64,204] \ No newline at end of file diff --git a/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/token-config.json b/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/token-config.json new file mode 100644 index 00000000000..7b01d74b471 --- /dev/null +++ b/dymension/solana_test2/environments/local-e2e/warp-routes/testwarproute/token-config.json @@ -0,0 +1,12 @@ +{ + "sealeveltest1": { + "type": "native", + "decimals": 9 + }, + "sealeveltest2": { + "type": "syntheticMemo", + "decimals": 9, + "name": "Solana", + "symbol": "SOL" + } +} \ No newline at end of file diff --git a/dymension/solana_test2/key.json b/dymension/solana_test2/key.json new file mode 100644 index 00000000000..0cb526d15e3 --- /dev/null +++ b/dymension/solana_test2/key.json @@ -0,0 +1 @@ +[7,43,157,20,5,71,179,108,120,72,181,64,167,47,224,55,23,234,68,103,249,188,2,227,150,102,192,184,130,241,215,247,21,132,194,37,186,203,67,214,141,144,83,185,27,60,19,84,216,212,254,115,156,182,235,156,70,114,22,166,221,41,209,251] \ No newline at end of file diff --git a/dymension/solana_test2/readme.md b/dymension/solana_test2/readme.md new file mode 100644 index 00000000000..f4f16631d71 --- /dev/null +++ b/dymension/solana_test2/readme.md @@ -0,0 +1,7 @@ +What is this? + +It's a test to check from the user perspective that: + +1. Deploying the memo program works +2. Transferring and including a memo works +3. Memo is correctly included in the outbound message diff --git a/dymension/solana_test2/scraper.json b/dymension/solana_test2/scraper.json new file mode 100644 index 00000000000..b7520f7ac4a --- /dev/null +++ b/dymension/solana_test2/scraper.json @@ -0,0 +1,28 @@ +{ + "metricsPort": 9093, + "chains": { + "sealeveltest1": { + "domain": 13375, + "connection": { + "type": "solana", + "urls": [ + ] + }, + "index": { + }, + "addresses": { + "mailbox": "0x77d43342b39f48f997dd7ad41df004f56f4c43d782b7872f69968484c7767ef3", + "interchainGasPaymaster": "0x556a615e0ce375b627c626d7439a2688a14d37f37bf88f10385fd9d9921e60a1", + "validatorAnnounce": "0x651268d47874bd59d9b15a7d40e7259c67ac9b64e71454d3f8554660d0d9693c", + "merkleTreeHook": "0x6f01e5a23c9e7138658371a691e634654107b4094136d1a6f1d71880f512e378" + } + } + }, + "chainsToScrape": [ + "sealeveltest1" + ], + "tracing": { + "level": "info", + "fmt": "pretty" + } +} \ No newline at end of file diff --git a/rust/main/chains/hyperlane-sealevel/src/mailbox_indexer.rs b/rust/main/chains/hyperlane-sealevel/src/mailbox_indexer.rs index 1d9503f853b..d02edfd5fd5 100644 --- a/rust/main/chains/hyperlane-sealevel/src/mailbox_indexer.rs +++ b/rust/main/chains/hyperlane-sealevel/src/mailbox_indexer.rs @@ -107,6 +107,8 @@ impl SealevelMailboxIndexer { let hyperlane_message = HyperlaneMessage::read_from(&mut &dispatched_message_account.encoded_message[..])?; + info!("foo encoded_message: {:?}", hyperlane_message); + let log_meta = if self.advanced_log_meta { self.dispatch_message_log_meta( U256::from(nonce), diff --git a/rust/sealevel/Cargo.lock b/rust/sealevel/Cargo.lock index 9fd71b04378..6a160c8036b 100644 --- a/rust/sealevel/Cargo.lock +++ b/rust/sealevel/Cargo.lock @@ -2457,8 +2457,11 @@ dependencies = [ "hyperlane-sealevel-multisig-ism-message-id", "hyperlane-sealevel-token", "hyperlane-sealevel-token-collateral", + "hyperlane-sealevel-token-collateral-memo", "hyperlane-sealevel-token-lib", + "hyperlane-sealevel-token-memo", "hyperlane-sealevel-token-native", + "hyperlane-sealevel-token-native-memo", "hyperlane-sealevel-validator-announce", "pretty_env_logger", "serde", @@ -2721,6 +2724,35 @@ dependencies = [ "thiserror", ] +[[package]] +name = "hyperlane-sealevel-token-collateral-memo" +version = "0.1.0" +dependencies = [ + "account-utils", + "borsh", + "hyperlane-core", + "hyperlane-sealevel-connection-client", + "hyperlane-sealevel-igp", + "hyperlane-sealevel-mailbox", + "hyperlane-sealevel-message-recipient-interface", + "hyperlane-sealevel-test-ism", + "hyperlane-sealevel-token-collateral", + "hyperlane-sealevel-token-lib", + "hyperlane-test-utils", + "hyperlane-warp-route", + "num-derive 0.4.2", + "num-traits", + "serializable-account-meta", + "solana-program", + "solana-program-test", + "solana-sdk", + "spl-associated-token-account", + "spl-noop", + "spl-token", + "spl-token-2022", + "thiserror", +] + [[package]] name = "hyperlane-sealevel-token-lib" version = "0.1.0" @@ -2745,6 +2777,34 @@ dependencies = [ "thiserror", ] +[[package]] +name = "hyperlane-sealevel-token-memo" +version = "0.1.0" +dependencies = [ + "account-utils", + "borsh", + "hyperlane-core", + "hyperlane-sealevel-connection-client", + "hyperlane-sealevel-igp", + "hyperlane-sealevel-mailbox", + "hyperlane-sealevel-message-recipient-interface", + "hyperlane-sealevel-test-ism", + "hyperlane-sealevel-token-lib", + "hyperlane-test-utils", + "hyperlane-warp-route", + "num-derive 0.4.2", + "num-traits", + "serializable-account-meta", + "solana-program", + "solana-program-test", + "solana-sdk", + "spl-associated-token-account", + "spl-noop", + "spl-token", + "spl-token-2022", + "thiserror", +] + [[package]] name = "hyperlane-sealevel-token-native" version = "0.1.0" @@ -2771,6 +2831,33 @@ dependencies = [ "thiserror", ] +[[package]] +name = "hyperlane-sealevel-token-native-memo" +version = "0.1.0" +dependencies = [ + "account-utils", + "borsh", + "hyperlane-core", + "hyperlane-sealevel-connection-client", + "hyperlane-sealevel-igp", + "hyperlane-sealevel-mailbox", + "hyperlane-sealevel-message-recipient-interface", + "hyperlane-sealevel-test-ism", + "hyperlane-sealevel-token-lib", + "hyperlane-sealevel-token-native", + "hyperlane-test-utils", + "hyperlane-warp-route", + "num-derive 0.4.2", + "num-traits", + "serializable-account-meta", + "solana-program", + "solana-program-test", + "solana-sdk", + "spl-noop", + "tarpc", + "thiserror", +] + [[package]] name = "hyperlane-sealevel-validator-announce" version = "0.1.0" diff --git a/rust/sealevel/Cargo.toml b/rust/sealevel/Cargo.toml index ffc287a2165..1c762a96e93 100644 --- a/rust/sealevel/Cargo.toml +++ b/rust/sealevel/Cargo.toml @@ -15,8 +15,11 @@ members = [ "programs/hyperlane-sealevel-igp", "programs/hyperlane-sealevel-igp-test", "programs/hyperlane-sealevel-token", + "programs/hyperlane-sealevel-token-memo", "programs/hyperlane-sealevel-token-collateral", + "programs/hyperlane-sealevel-token-collateral-memo", "programs/hyperlane-sealevel-token-native", + "programs/hyperlane-sealevel-token-native-memo", "programs/ism/multisig-ism-message-id", "programs/ism/test-ism", "programs/mailbox", diff --git a/rust/sealevel/client/Cargo.toml b/rust/sealevel/client/Cargo.toml index b799093aeac..451dc3022ac 100644 --- a/rust/sealevel/client/Cargo.toml +++ b/rust/sealevel/client/Cargo.toml @@ -35,6 +35,9 @@ hyperlane-sealevel-multisig-ism-message-id = { path = "../programs/ism/multisig- hyperlane-sealevel-token = { path = "../programs/hyperlane-sealevel-token", features = [ "no-entrypoint", ] } +hyperlane-sealevel-token-memo = { path = "../programs/hyperlane-sealevel-token-memo", features = [ + "no-entrypoint", +] } hyperlane-sealevel-igp = { path = "../programs/hyperlane-sealevel-igp", features = [ "no-entrypoint", "serde", @@ -42,10 +45,16 @@ hyperlane-sealevel-igp = { path = "../programs/hyperlane-sealevel-igp", features hyperlane-sealevel-token-collateral = { path = "../programs/hyperlane-sealevel-token-collateral", features = [ "no-entrypoint", ] } +hyperlane-sealevel-token-collateral-memo = { path = "../programs/hyperlane-sealevel-token-collateral-memo", features = [ + "no-entrypoint", +] } hyperlane-sealevel-token-lib = { path = "../libraries/hyperlane-sealevel-token" } hyperlane-sealevel-token-native = { path = "../programs/hyperlane-sealevel-token-native", features = [ "no-entrypoint", ] } +hyperlane-sealevel-token-native-memo = { path = "../programs/hyperlane-sealevel-token-native-memo", features = [ + "no-entrypoint", +] } hyperlane-sealevel-validator-announce = { path = "../programs/validator-announce", features = [ "no-entrypoint", ] } diff --git a/rust/sealevel/client/src/main.rs b/rust/sealevel/client/src/main.rs index 626d45e0e8f..4f514ed3b5e 100644 --- a/rust/sealevel/client/src/main.rs +++ b/rust/sealevel/client/src/main.rs @@ -43,12 +43,23 @@ use hyperlane_sealevel_token::{ use hyperlane_sealevel_token_collateral::{ hyperlane_token_escrow_pda_seeds, plugin::CollateralPlugin, }; +use hyperlane_sealevel_token_collateral_memo::hyperlane_token_escrow_pda_seeds as hyperlane_token_escrow_pda_seeds_memo; use hyperlane_sealevel_token_lib::{ accounts::HyperlaneTokenAccount, hyperlane_token_pda_seeds, - instruction::{Instruction as HtInstruction, TransferRemote as HtTransferRemote}, + instruction::{ + DymInstruction as DymHtInstruction, Instruction as HtInstruction, + TransferRemote as HtTransferRemote, TransferRemoteMemo as DymHtTransferRemoteMemo, + }, +}; +use hyperlane_sealevel_token_memo::{ + hyperlane_token_ata_payer_pda_seeds as hyperlane_token_ata_payer_pda_seeds_memo, + hyperlane_token_mint_pda_seeds as hyperlane_token_mint_pda_seeds_memo, + spl_associated_token_account::get_associated_token_address_with_program_id as get_associated_token_address_with_program_id_memo, + spl_token_2022 as spl_token_2022_memo, }; use hyperlane_sealevel_token_native::hyperlane_token_native_collateral_pda_seeds; +use hyperlane_sealevel_token_native_memo::hyperlane_token_native_collateral_pda_seeds as hyperlane_token_native_memo_collateral_pda_seeds; use hyperlane_sealevel_validator_announce::{ accounts::ValidatorStorageLocationsAccount, instruction::{ @@ -295,6 +306,7 @@ struct TokenCmd { enum TokenSubCmd { Query(TokenQuery), TransferRemote(TokenTransferRemote), + TransferRemoteMemo(TokenTransferRemoteMemo), EnrollRemoteRouter(TokenEnrollRemoteRouter), TransferOwnership(TransferOwnership), SetInterchainSecurityModule(SetInterchainSecurityModule), @@ -304,8 +316,11 @@ enum TokenSubCmd { #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] pub enum TokenType { Native, + NativeMemo, Synthetic, + SyntheticMemo, Collateral, + CollateralMemo, } #[derive(Args)] @@ -329,6 +344,21 @@ struct TokenTransferRemote { token_type: TokenType, } +#[derive(Args)] +// TODO: would have been nice to not duplicate +struct TokenTransferRemoteMemo { + #[arg(long, short, default_value_t = HYPERLANE_TOKEN_PROG_ID)] + program_id: Pubkey, + // Note this is the keypair for normal account not the derived associated token account or delegate. + sender: String, + amount: u64, + destination_domain: u32, + recipient: String, + #[arg(value_enum)] + token_type: TokenType, + memo: String, +} + #[derive(Args)] struct TokenEnrollRemoteRouter { #[arg(long, short, default_value_t = HYPERLANE_TOKEN_PROG_ID)] @@ -926,6 +956,14 @@ fn process_token_cmd(mut ctx: Context, cmd: TokenCmd) { ); accounts_to_query.push(native_collateral_account); } + TokenType::NativeMemo => { + let (native_collateral_account, _native_collateral_bump) = + Pubkey::find_program_address( + hyperlane_token_native_memo_collateral_pda_seeds!(), + &query.program_id, + ); + accounts_to_query.push(native_collateral_account); + } TokenType::Synthetic => { let (mint_account, _mint_bump) = Pubkey::find_program_address( hyperlane_token_mint_pda_seeds!(), @@ -938,6 +976,18 @@ fn process_token_cmd(mut ctx: Context, cmd: TokenCmd) { accounts_to_query.push(mint_account); accounts_to_query.push(ata_payer_account); } + TokenType::SyntheticMemo => { + let (mint_account, _mint_bump) = Pubkey::find_program_address( + hyperlane_token_mint_pda_seeds_memo!(), + &query.program_id, + ); + let (ata_payer_account, _ata_payer_bump) = Pubkey::find_program_address( + hyperlane_token_ata_payer_pda_seeds_memo!(), + &query.program_id, + ); + accounts_to_query.push(mint_account); + accounts_to_query.push(ata_payer_account); + } TokenType::Collateral => { let (escrow_account, _escrow_bump) = Pubkey::find_program_address( hyperlane_token_escrow_pda_seeds!(), @@ -945,6 +995,13 @@ fn process_token_cmd(mut ctx: Context, cmd: TokenCmd) { ); accounts_to_query.push(escrow_account); } + TokenType::CollateralMemo => { + let (escrow_account, _escrow_bump) = Pubkey::find_program_address( + hyperlane_token_escrow_pda_seeds_memo!(), + &query.program_id, + ); + accounts_to_query.push(escrow_account); + } } let accounts = ctx @@ -984,6 +1041,23 @@ fn process_token_cmd(mut ctx: Context, cmd: TokenCmd) { } println!("--------------------------------"); } + TokenType::NativeMemo => { + let (native_collateral_account, native_collateral_bump) = + Pubkey::find_program_address( + hyperlane_token_native_memo_collateral_pda_seeds!(), + &query.program_id, + ); + println!( + "Native Token Memo Collateral: {}, bump={}", + native_collateral_account, native_collateral_bump + ); + if let Some(info) = &accounts[1] { + println!("{:#?}", info); + } else { + println!("Not yet created?"); + } + println!("--------------------------------"); + } TokenType::Synthetic => { let (mint_account, mint_bump) = Pubkey::find_program_address( hyperlane_token_mint_pda_seeds!(), @@ -1013,6 +1087,35 @@ fn process_token_cmd(mut ctx: Context, cmd: TokenCmd) { ata_payer_account, ata_payer_bump, ); } + TokenType::SyntheticMemo => { + let (mint_account, mint_bump) = Pubkey::find_program_address( + hyperlane_token_mint_pda_seeds_memo!(), + &query.program_id, + ); + println!( + "Mint / Mint Authority: {}, bump={}", + mint_account, mint_bump + ); + if let Some(info) = &accounts[1] { + println!("{:#?}", info); + use solana_program::program_pack::Pack as _; + match spl_token_2022::state::Mint::unpack_from_slice(info.data.as_ref()) { + Ok(mint) => println!("{:#?}", mint), + Err(err) => println!("Failed to deserialize account data: {}", err), + } + } else { + println!("Not yet created?"); + } + + let (ata_payer_account, ata_payer_bump) = Pubkey::find_program_address( + hyperlane_token_ata_payer_pda_seeds_memo!(), + &query.program_id, + ); + println!( + "ATA payer account: {}, bump={}", + ata_payer_account, ata_payer_bump, + ); + } TokenType::Collateral => { let (escrow_account, escrow_bump) = Pubkey::find_program_address( hyperlane_token_escrow_pda_seeds!(), @@ -1029,6 +1132,27 @@ fn process_token_cmd(mut ctx: Context, cmd: TokenCmd) { &query.program_id, ); + println!( + "ATA payer account: {}, bump={}", + ata_payer_account, ata_payer_bump, + ); + } + TokenType::CollateralMemo => { + let (escrow_account, escrow_bump) = Pubkey::find_program_address( + hyperlane_token_escrow_pda_seeds_memo!(), + &query.program_id, + ); + + println!( + "escrow_account (key, bump)=({}, {})", + escrow_account, escrow_bump, + ); + + let (ata_payer_account, ata_payer_bump) = Pubkey::find_program_address( + hyperlane_token_ata_payer_pda_seeds!(), + &query.program_id, + ); + println!( "ATA payer account: {}, bump={}", ata_payer_account, ata_payer_bump, @@ -1166,6 +1290,19 @@ fn process_token_cmd(mut ctx: Context, cmd: TokenCmd) { AccountMeta::new(native_collateral_account, false), ]); } + TokenType::NativeMemo => { + // 5. [executable] The system program. + // 6. [writeable] The native token collateral PDA account. + let (native_collateral_account, _native_collateral_bump) = + Pubkey::find_program_address( + hyperlane_token_native_memo_collateral_pda_seeds!(), + &xfer.program_id, + ); + accounts.extend([ + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new(native_collateral_account, false), + ]); + } TokenType::Synthetic => { // 5. [executable] The spl_token_2022 program. // 6. [writeable] The mint / mint authority PDA account. @@ -1186,7 +1323,253 @@ fn process_token_cmd(mut ctx: Context, cmd: TokenCmd) { AccountMeta::new(sender_associated_token_account, false), ]); } - TokenType::Collateral => { + TokenType::SyntheticMemo => { + // 5. [executable] The spl_token_2022 program. + // 6. [writeable] The mint / mint authority PDA account. + // 7. [writeable] The token sender's associated token account, from which tokens will be burned. + let (mint_account, _mint_bump) = Pubkey::find_program_address( + hyperlane_token_mint_pda_seeds_memo!(), + &xfer.program_id, + ); + let sender_associated_token_account = + get_associated_token_address_with_program_id_memo( + &sender.pubkey(), + &mint_account, + &spl_token_2022::id(), + ); + accounts.extend([ + AccountMeta::new_readonly(spl_token_2022::id(), false), + AccountMeta::new(mint_account, false), + AccountMeta::new(sender_associated_token_account, false), + ]); + } + TokenType::Collateral | TokenType::CollateralMemo => { + // 5. [executable] The SPL token program for the mint. + // 6. [writeable] The mint. + // 7. [writeable] The token sender's associated token account, from which tokens will be sent. + // 8. [writeable] The escrow PDA account. + let token = HyperlaneTokenAccount::::fetch( + &mut &fetched_token_account.data[..], + ) + .unwrap() + .into_inner(); + let sender_associated_token_account = + get_associated_token_address_with_program_id( + // DYMENSION: BUG? ASSUMES SYNTHETIC USED ON OTHER CHAIN + &sender.pubkey(), + &token.plugin_data.mint, + &token.plugin_data.spl_token_program, + ); + accounts.extend([ + AccountMeta::new_readonly(token.plugin_data.spl_token_program, false), + AccountMeta::new(token.plugin_data.mint, false), + AccountMeta::new(sender_associated_token_account, false), + AccountMeta::new(token.plugin_data.escrow, false), + ]); + } + } + + eprintln!("accounts={:#?}", accounts); // FIXME remove + let xfer_instruction = Instruction { + program_id: xfer.program_id, + data: ixn.encode().unwrap(), + accounts, + }; + let tx_result = ctx.new_txn().add(xfer_instruction).send(&[ + &*ctx.payer_signer(), + &sender, + &unique_message_account_keypair, + ]); + // Print the output so it can be used in e2e tests + println!("{:?}", tx_result); + } + TokenSubCmd::TransferRemoteMemo(xfer) => { + is_keypair(&xfer.sender).unwrap(); + ctx.commitment = CommitmentConfig::finalized(); + let sender = read_keypair_file(xfer.sender).unwrap(); + + let recipient = if xfer.recipient.starts_with("0x") { + H256::from_str(&xfer.recipient).unwrap() + } else { + let pubkey = Pubkey::from_str(&xfer.recipient).unwrap(); + H256::from_slice(&pubkey.to_bytes()[..]) + }; + + let (token_account, _token_bump) = + Pubkey::find_program_address(hyperlane_token_pda_seeds!(), &xfer.program_id); + let (dispatch_authority_account, _dispatch_authority_bump) = + Pubkey::find_program_address( + mailbox_message_dispatch_authority_pda_seeds!(), + &xfer.program_id, + ); + + let fetched_token_account = ctx + .client + .get_account_with_commitment(&token_account, ctx.commitment) + .unwrap() + .value + .unwrap(); + let token = HyperlaneTokenAccount::<()>::fetch(&mut &fetched_token_account.data[..]) + .unwrap() + .into_inner(); + + let unique_message_account_keypair = Keypair::new(); + let (dispatched_message_account, _dispatched_message_bump) = + Pubkey::find_program_address( + mailbox_dispatched_message_pda_seeds!(&unique_message_account_keypair.pubkey()), + &token.mailbox, + ); + + let (mailbox_outbox_account, _mailbox_outbox_bump) = + Pubkey::find_program_address(mailbox_outbox_pda_seeds!(), &token.mailbox); + + let ixn = DymHtInstruction::TransferRemoteMemo(DymHtTransferRemoteMemo { + base: HtTransferRemote { + destination_domain: xfer.destination_domain, + recipient, + amount_or_id: xfer.amount.into(), + }, + memo: xfer.memo.as_bytes().to_vec(), // TODO: conversion OK? + }); + + // Transfers tokens to a remote. + // Burns the tokens from the sender's associated token account and + // then dispatches a message to the remote recipient. + // + // 0. [executable] The system program. + // 1. [executable] The spl_noop program. + // 2. [] The token PDA account. + // 3. [executable] The mailbox program. + // 4. [writeable] The mailbox outbox account. + // 5. [] Message dispatch authority. + // 6. [signer] The token sender and mailbox payer. + // 7. [signer] Unique message / gas payment account. + // 8. [writeable] Message storage PDA. + // ---- If using an IGP ---- + // 9. [executable] The IGP program. + // 10. [writeable] The IGP program data. + // 11. [writeable] Gas payment PDA. + // 12. [] OPTIONAL - The Overhead IGP program, if the configured IGP is an Overhead IGP. + // 13. [writeable] The IGP account. + // ---- End if ---- + // 14..N [??..??] Plugin-specific accounts. + let mut accounts = vec![ + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(spl_noop::id(), false), + AccountMeta::new_readonly(token_account, false), + AccountMeta::new_readonly(token.mailbox, false), + AccountMeta::new(mailbox_outbox_account, false), + AccountMeta::new_readonly(dispatch_authority_account, false), + AccountMeta::new(sender.pubkey(), true), + AccountMeta::new_readonly(unique_message_account_keypair.pubkey(), true), + AccountMeta::new(dispatched_message_account, false), + ]; + + if let Some((igp_program_id, igp_account_type)) = token.interchain_gas_paymaster { + let (igp_program_data, _bump) = + Pubkey::find_program_address(igp_program_data_pda_seeds!(), &igp_program_id); + let (gas_payment_pda, _bump) = Pubkey::find_program_address( + igp_gas_payment_pda_seeds!(&unique_message_account_keypair.pubkey()), + &igp_program_id, + ); + + accounts.extend([ + AccountMeta::new_readonly(igp_program_id, false), + AccountMeta::new(igp_program_data, false), + AccountMeta::new(gas_payment_pda, false), + ]); + + match igp_account_type { + InterchainGasPaymasterType::OverheadIgp(overhead_igp_account_id) => { + let overhead_igp_account = ctx + .client + .get_account_with_commitment(&overhead_igp_account_id, ctx.commitment) + .unwrap() + .value + .unwrap(); + let overhead_igp_account = + OverheadIgpAccount::fetch(&mut &overhead_igp_account.data[..]) + .unwrap() + .into_inner(); + accounts.extend([ + AccountMeta::new_readonly(overhead_igp_account_id, false), + AccountMeta::new(overhead_igp_account.inner, false), + ]); + } + InterchainGasPaymasterType::Igp(igp_account_id) => { + accounts.push(AccountMeta::new(igp_account_id, false)); + } + } + } + + match xfer.token_type { + TokenType::Native => { + // 5. [executable] The system program. + // 6. [writeable] The native token collateral PDA account. + let (native_collateral_account, _native_collateral_bump) = + Pubkey::find_program_address( + hyperlane_token_native_collateral_pda_seeds!(), + &xfer.program_id, + ); + accounts.extend([ + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new(native_collateral_account, false), + ]); + } + TokenType::NativeMemo => { + // 5. [executable] The system program. + // 6. [writeable] The native token collateral PDA account. + let (native_collateral_account, _native_collateral_bump) = + Pubkey::find_program_address( + hyperlane_token_native_memo_collateral_pda_seeds!(), + &xfer.program_id, + ); + accounts.extend([ + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new(native_collateral_account, false), + ]); + } + TokenType::Synthetic => { + // 5. [executable] The spl_token_2022 program. + // 6. [writeable] The mint / mint authority PDA account. + // 7. [writeable] The token sender's associated token account, from which tokens will be burned. + let (mint_account, _mint_bump) = Pubkey::find_program_address( + hyperlane_token_mint_pda_seeds!(), + &xfer.program_id, + ); + let sender_associated_token_account = + get_associated_token_address_with_program_id( + &sender.pubkey(), + &mint_account, + &spl_token_2022::id(), + ); + accounts.extend([ + AccountMeta::new_readonly(spl_token_2022::id(), false), + AccountMeta::new(mint_account, false), + AccountMeta::new(sender_associated_token_account, false), + ]); + } + TokenType::SyntheticMemo => { + // 5. [executable] The spl_token_2022 program. + // 6. [writeable] The mint / mint authority PDA account. + // 7. [writeable] The token sender's associated token account, from which tokens will be burned. + let (mint_account, _mint_bump) = Pubkey::find_program_address( + hyperlane_token_mint_pda_seeds_memo!(), + &xfer.program_id, + ); + let sender_associated_token_account = + get_associated_token_address_with_program_id_memo( + &sender.pubkey(), + &mint_account, + &spl_token_2022::id(), + ); + accounts.extend([ + AccountMeta::new_readonly(spl_token_2022::id(), false), + AccountMeta::new(mint_account, false), + AccountMeta::new(sender_associated_token_account, false), + ]); + } + TokenType::Collateral | TokenType::CollateralMemo => { // 5. [executable] The SPL token program for the mint. // 6. [writeable] The mint. // 7. [writeable] The token sender's associated token account, from which tokens will be sent. @@ -1198,6 +1581,7 @@ fn process_token_cmd(mut ctx: Context, cmd: TokenCmd) { .into_inner(); let sender_associated_token_account = get_associated_token_address_with_program_id( + // DYMENSION: BUG? ASSUMES SYNTHETIC USED ON OTHER CHAIN &sender.pubkey(), &token.plugin_data.mint, &token.plugin_data.spl_token_program, @@ -1225,6 +1609,7 @@ fn process_token_cmd(mut ctx: Context, cmd: TokenCmd) { // Print the output so it can be used in e2e tests println!("{:?}", tx_result); } + TokenSubCmd::EnrollRemoteRouter(enroll) => { let enroll_instruction = HtInstruction::EnrollRemoteRouter(RemoteRouterConfig { domain: enroll.domain, diff --git a/rust/sealevel/client/src/warp_route.rs b/rust/sealevel/client/src/warp_route.rs index 60843741f74..04527f8ad5a 100644 --- a/rust/sealevel/client/src/warp_route.rs +++ b/rust/sealevel/client/src/warp_route.rs @@ -100,8 +100,11 @@ impl DecimalMetadata { #[serde(tag = "type", rename_all = "camelCase")] enum TokenType { Native, + NativeMemo, Synthetic(TokenMetadata), + SyntheticMemo(TokenMetadata), Collateral(CollateralInfo), + CollateralMemo(CollateralInfo), } impl TokenType { @@ -112,8 +115,11 @@ impl TokenType { // enforce gas amounts to Sealevel chains. match &self { TokenType::Synthetic(_) => 64_000, + TokenType::SyntheticMemo(_) => 64_000, TokenType::Native => 44_000, + TokenType::NativeMemo => 44_000, TokenType::Collateral(_) => 68_000, + TokenType::CollateralMemo(_) => 68_000, } } } @@ -188,8 +194,11 @@ impl RouterDeployer for WarpRouteDeployer { fn program_name(&self, config: &TokenConfig) -> &str { match config.token_type { TokenType::Native => "hyperlane_sealevel_token_native", + TokenType::NativeMemo => "hyperlane_sealevel_token_native_memo", TokenType::Synthetic(_) => "hyperlane_sealevel_token", + TokenType::SyntheticMemo(_) => "hyperlane_sealevel_token_memo", TokenType::Collateral(_) => "hyperlane_sealevel_token_collateral", + TokenType::CollateralMemo(_) => "hyperlane_sealevel_token_collateral_memo", } } @@ -291,6 +300,14 @@ impl RouterDeployer for WarpRouteDeployer { ) .unwrap(), ), + TokenType::NativeMemo => ctx.new_txn().add( + hyperlane_sealevel_token_native_memo::instruction::init_instruction( + program_id, + ctx.payer_pubkey, + init, + ) + .unwrap(), + ), TokenType::Synthetic(_token_metadata) => { let decimals = init.decimals; @@ -346,6 +363,61 @@ impl RouterDeployer for WarpRouteDeployer { .unwrap(), ) } + TokenType::SyntheticMemo(_token_metadata) => { + let decimals = init.decimals; + + ctx.new_txn() + .add( + hyperlane_sealevel_token_memo::instruction::init_instruction( + program_id, + ctx.payer_pubkey, + init, + ) + .unwrap(), + ) + .with_client(client) + .send_with_payer(); + + let (mint_account, _mint_bump) = + Pubkey::find_program_address(hyperlane_token_mint_pda_seeds!(), &program_id); + + let mut cmd = Command::new(spl_token_binary_path.clone()); + cmd.args([ + "create-token", + mint_account.to_string().as_str(), + "--enable-metadata", + "-p", + spl_token_2022::id().to_string().as_str(), + "--url", + client.url().as_str(), + "--with-compute-unit-limit", + "500000", + "--mint-authority", + &ctx.payer_pubkey.to_string(), + "--fee-payer", + ctx.payer_keypair_path(), + ]); + + println!("running command: {:?}", cmd); + let status = cmd + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .status() + .expect("Failed to run command"); + + println!("initialized metadata pointer. Status: {status}"); + + ctx.new_txn().add( + spl_token_2022::instruction::initialize_mint2( + &spl_token_2022::id(), + &mint_account, + &ctx.payer_pubkey, + None, + decimals, + ) + .unwrap(), + ) + } TokenType::Collateral(collateral_info) => { let collateral_mint = collateral_info.mint.parse().expect("Invalid mint address"); let collateral_mint_account = client.get_account(&collateral_mint).unwrap(); @@ -364,6 +436,24 @@ impl RouterDeployer for WarpRouteDeployer { .unwrap(), ) } + TokenType::CollateralMemo(collateral_info) => { + let collateral_mint = collateral_info.mint.parse().expect("Invalid mint address"); + let collateral_mint_account = client.get_account(&collateral_mint).unwrap(); + // The owner of the mint account is the SPL Token program responsible for it + // (either spl-token or spl-token-2022). + let collateral_spl_token_program = collateral_mint_account.owner; + + ctx.new_txn().add( + hyperlane_sealevel_token_collateral_memo::instruction::init_instruction( + program_id, + ctx.payer_pubkey, + init, + collateral_spl_token_program, + collateral_mint, + ) + .unwrap(), + ) + } } .with_client(client) .send_with_payer(); @@ -712,15 +802,15 @@ pub fn parse_token_account_data(token_type: FlatTokenType, data: &mut &[u8]) { } match token_type { - FlatTokenType::Native => { + FlatTokenType::Native | FlatTokenType::NativeMemo => { let res = HyperlaneTokenAccount::::fetch(data); print_data_or_err(res); } - FlatTokenType::Synthetic => { + FlatTokenType::Synthetic | FlatTokenType::SyntheticMemo => { let res = HyperlaneTokenAccount::::fetch(data); print_data_or_err(res); } - FlatTokenType::Collateral => { + FlatTokenType::Collateral | FlatTokenType::CollateralMemo => { let res = HyperlaneTokenAccount::::fetch(data); print_data_or_err(res); } diff --git a/rust/sealevel/environments/testnet4/warp-routes/foowarp/token-config.json b/rust/sealevel/environments/testnet4/warp-routes/foowarp/token-config.json new file mode 100644 index 00000000000..aea499582a3 --- /dev/null +++ b/rust/sealevel/environments/testnet4/warp-routes/foowarp/token-config.json @@ -0,0 +1,7 @@ +{ + "solanatestnet": { + "type": "nativeMemo", + "decimals": 9, + "interchainGasPaymaster": "9SQVtTNsbipdMzumhzi6X8GwojiSMwBfqAhS7FgyTcqy" + } +} diff --git a/rust/sealevel/libraries/hyperlane-sealevel-token/src/instruction.rs b/rust/sealevel/libraries/hyperlane-sealevel-token/src/instruction.rs index 2193ea355da..ffa86ebd981 100644 --- a/rust/sealevel/libraries/hyperlane-sealevel-token/src/instruction.rs +++ b/rust/sealevel/libraries/hyperlane-sealevel-token/src/instruction.rs @@ -42,6 +42,29 @@ impl DiscriminatorData for Instruction { const DISCRIMINATOR: [u8; Self::DISCRIMINATOR_LENGTH] = PROGRAM_INSTRUCTION_DISCRIMINATOR; } +// ~~~~~~~~~~~~~~~~ DYMENSION ~~~~~~~~~~~~~~~~~~ + +/// Instruction data for transferring `amount_or_id` token to `recipient` +/// on `destination` domain, including a memo. +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)] +pub struct TransferRemoteMemo { + /// Base transfer instruction. + pub base: TransferRemote, + /// Arbitrary metadata. + pub memo: Vec, +} + +/// Instructions specifically for this token program +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)] +pub enum DymInstruction { + /// Transfer tokens to a remote recipient, including a memo. + TransferRemoteMemo(TransferRemoteMemo), +} + +impl DiscriminatorData for DymInstruction { + const DISCRIMINATOR: [u8; Self::DISCRIMINATOR_LENGTH] = PROGRAM_INSTRUCTION_DISCRIMINATOR; +} + /// Instruction data for initializing the program. #[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)] pub struct Init { diff --git a/rust/sealevel/libraries/hyperlane-sealevel-token/src/processor.rs b/rust/sealevel/libraries/hyperlane-sealevel-token/src/processor.rs index 09e19d9339c..6ea3c2ded24 100644 --- a/rust/sealevel/libraries/hyperlane-sealevel-token/src/processor.rs +++ b/rust/sealevel/libraries/hyperlane-sealevel-token/src/processor.rs @@ -618,6 +618,226 @@ where Ok(()) } + /// Transfers tokens to a remote. Copy of transfer remote with memo added. + pub fn transfer_remote_memo( + program_id: &Pubkey, + accounts: &[AccountInfo], + xfer: TransferRemote, + memo: Vec, + ) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: System program. + let system_program_account = next_account_info(accounts_iter)?; + if system_program_account.key != &solana_program::system_program::id() { + return Err(ProgramError::InvalidArgument); + } + + // Account 1: SPL Noop. + let spl_noop = next_account_info(accounts_iter)?; + if spl_noop.key != &spl_noop::id() { + return Err(ProgramError::InvalidArgument); + } + + // Account 2: Token storage account + let token_account = next_account_info(accounts_iter)?; + let token = + HyperlaneTokenAccount::fetch(&mut &token_account.data.borrow()[..])?.into_inner(); + let token_seeds: &[&[u8]] = hyperlane_token_pda_seeds!(token.bump); + let expected_token_key = Pubkey::create_program_address(token_seeds, program_id)?; + if token_account.key != &expected_token_key { + return Err(ProgramError::InvalidArgument); + } + if token_account.owner != program_id { + return Err(ProgramError::IncorrectProgramId); + } + + // Account 3: Mailbox program + let mailbox_info = next_account_info(accounts_iter)?; + if mailbox_info.key != &token.mailbox { + return Err(ProgramError::IncorrectProgramId); + } + + // Account 4: Mailbox Outbox data account. + // No verification is performed here, the Mailbox will do that. + let mailbox_outbox_account = next_account_info(accounts_iter)?; + + // Account 5: Message dispatch authority + let dispatch_authority_account = next_account_info(accounts_iter)?; + let dispatch_authority_seeds: &[&[u8]] = + mailbox_message_dispatch_authority_pda_seeds!(token.dispatch_authority_bump); + let dispatch_authority_key = + Pubkey::create_program_address(dispatch_authority_seeds, program_id)?; + if *dispatch_authority_account.key != dispatch_authority_key { + return Err(ProgramError::InvalidArgument); + } + + // Account 6: Sender account / mailbox payer + let sender_wallet = next_account_info(accounts_iter)?; + if !sender_wallet.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + // Account 7: Unique message / gas payment account + // Defer to the checks in the Mailbox / IGP, no need to verify anything here. + let unique_message_account = next_account_info(accounts_iter)?; + + // Account 8: Message storage PDA. + // Similarly defer to the checks in the Mailbox to ensure account validity. + let dispatched_message_pda = next_account_info(accounts_iter)?; + + let igp_payment_accounts = + if let Some((igp_program_id, igp_account_type)) = token.interchain_gas_paymaster() { + // Account 9: The IGP program + let igp_program_account = next_account_info(accounts_iter)?; + if igp_program_account.key != igp_program_id { + return Err(ProgramError::InvalidArgument); + } + + // Account 10: The IGP program data. + // No verification is performed here, the IGP will do that. + let igp_program_data_account = next_account_info(accounts_iter)?; + + // Account 11: The gas payment PDA. + // No verification is performed here, the IGP will do that. + let igp_payment_pda_account = next_account_info(accounts_iter)?; + + // Account 12: The configured IGP account. + let configured_igp_account = next_account_info(accounts_iter)?; + if configured_igp_account.key != igp_account_type.key() { + return Err(ProgramError::InvalidArgument); + } + + // Accounts expected by the IGP's `PayForGas` instruction: + // + // 0. `[executable]` The system program. + // 1. `[signer]` The payer. + // 2. `[writeable]` The IGP program data. + // 3. `[signer]` Unique gas payment account. + // 4. `[writeable]` Gas payment PDA. + // 5. `[writeable]` The IGP account. + // 6. `[]` Overhead IGP account (optional). + + let mut igp_payment_account_metas = vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(*sender_wallet.key, true), + AccountMeta::new(*igp_program_data_account.key, false), + AccountMeta::new_readonly(*unique_message_account.key, true), + AccountMeta::new(*igp_payment_pda_account.key, false), + ]; + let mut igp_payment_account_infos = vec![ + system_program_account.clone(), + sender_wallet.clone(), + igp_program_data_account.clone(), + unique_message_account.clone(), + igp_payment_pda_account.clone(), + ]; + + match igp_account_type { + InterchainGasPaymasterType::Igp(_) => { + igp_payment_account_metas + .push(AccountMeta::new(*configured_igp_account.key, false)); + igp_payment_account_infos.push(configured_igp_account.clone()); + } + InterchainGasPaymasterType::OverheadIgp(_) => { + // Account 13: The inner IGP account. + let inner_igp_account = next_account_info(accounts_iter)?; + + // The inner IGP is expected first, then the overhead IGP. + igp_payment_account_metas.extend([ + AccountMeta::new(*inner_igp_account.key, false), + AccountMeta::new_readonly(*configured_igp_account.key, false), + ]); + igp_payment_account_infos + .extend([inner_igp_account.clone(), configured_igp_account.clone()]); + } + }; + + Some((igp_payment_account_metas, igp_payment_account_infos)) + } else { + None + }; + + // The amount denominated in the local decimals. + let local_amount: u64 = xfer + .amount_or_id + .try_into() + .map_err(|_| Error::IntegerOverflow)?; + // Convert to the remote number of decimals, which is universally understood + // by the remote routers as the number of decimals used by the message amount. + let remote_amount = token.local_amount_to_remote_amount(local_amount)?; + + // Transfer `local_amount` of tokens in... + T::transfer_in( + program_id, + &*token, + sender_wallet, + accounts_iter, + local_amount, + )?; + + if accounts_iter.next().is_some() { + return Err(ProgramError::from(Error::ExtraneousAccount)); + } + + let dispatch_account_metas = vec![ + AccountMeta::new(*mailbox_outbox_account.key, false), + AccountMeta::new_readonly(*dispatch_authority_account.key, true), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new_readonly(spl_noop::id(), false), + AccountMeta::new(*sender_wallet.key, true), + AccountMeta::new_readonly(*unique_message_account.key, true), + AccountMeta::new(*dispatched_message_pda.key, false), + ]; + let dispatch_account_infos = &[ + mailbox_outbox_account.clone(), + dispatch_authority_account.clone(), + system_program_account.clone(), + spl_noop.clone(), + sender_wallet.clone(), + unique_message_account.clone(), + dispatched_message_pda.clone(), + ]; + + // The token message body, which specifies the remote_amount. + let token_transfer_message = + TokenMessage::new(xfer.recipient, remote_amount, memo).to_vec(); + + if let Some((igp_payment_account_metas, igp_payment_account_infos)) = igp_payment_accounts { + // Dispatch the message and pay for gas. + HyperlaneGasRouterDispatch::dispatch_with_gas( + &*token, + program_id, + dispatch_authority_seeds, + xfer.destination_domain, + token_transfer_message, + dispatch_account_metas, + dispatch_account_infos, + igp_payment_account_metas, + &igp_payment_account_infos, + )?; + } else { + // Dispatch the message. + token.dispatch( + program_id, + dispatch_authority_seeds, + xfer.destination_domain, + token_transfer_message, + dispatch_account_metas, + dispatch_account_infos, + )?; + } + + msg!( + "Warp route transfer completed to destination: {}, recipient: {}, remote_amount: {}", + xfer.destination_domain, + xfer.recipient, + remote_amount + ); + + Ok(()) + } + /// Enrolls a remote router. /// /// Accounts: diff --git a/rust/sealevel/programs/build-programs.sh b/rust/sealevel/programs/build-programs.sh index c6dcaef4dc5..45881ff416d 100755 --- a/rust/sealevel/programs/build-programs.sh +++ b/rust/sealevel/programs/build-programs.sh @@ -13,7 +13,7 @@ SOLANA_CLI_VERSION_FOR_BUILDING_PROGRAMS="1.14.20" # The paths to the programs CORE_PROGRAM_PATHS=("mailbox" "ism/multisig-ism-message-id" "validator-announce" "hyperlane-sealevel-igp") -TOKEN_PROGRAM_PATHS=("hyperlane-sealevel-token" "hyperlane-sealevel-token-collateral" "hyperlane-sealevel-token-native") +TOKEN_PROGRAM_PATHS=("hyperlane-sealevel-token" "hyperlane-sealevel-token-collateral" "hyperlane-sealevel-token-native" "hyperlane-sealevel-token-memo" "hyperlane-sealevel-token-collateral-memo" "hyperlane-sealevel-token-native-memo") build_program () { PROGRAM_PATH=$1 diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-collateral-memo/Cargo.toml b/rust/sealevel/programs/hyperlane-sealevel-token-collateral-memo/Cargo.toml new file mode 100644 index 00000000000..46d1ca3306b --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-collateral-memo/Cargo.toml @@ -0,0 +1,47 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "hyperlane-sealevel-token-collateral-memo" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] + +[dependencies] +borsh.workspace = true +num-derive.workspace = true +num-traits.workspace = true +solana-program.workspace = true +spl-associated-token-account.workspace = true +spl-noop.workspace = true +spl-token-2022.workspace = true # FIXME Should we actually use 2022 here or try normal token program? +spl-token.workspace = true +thiserror.workspace = true + +account-utils = { path = "../../libraries/account-utils" } +hyperlane-core = { path = "../../../main/hyperlane-core" } +hyperlane-sealevel-connection-client = { path = "../../libraries/hyperlane-sealevel-connection-client" } +hyperlane-sealevel-mailbox = { path = "../mailbox", features = [ + "no-entrypoint", +] } +hyperlane-sealevel-igp = { path = "../hyperlane-sealevel-igp", features = [ + "no-entrypoint", +] } +hyperlane-sealevel-message-recipient-interface = { path = "../../libraries/message-recipient-interface" } +hyperlane-sealevel-token-lib = { path = "../../libraries/hyperlane-sealevel-token" } +hyperlane-warp-route = { path = "../../../main/applications/hyperlane-warp-route" } +serializable-account-meta = { path = "../../libraries/serializable-account-meta" } +hyperlane-sealevel-token-collateral = { path = "../hyperlane-sealevel-token-collateral" } + +[dev-dependencies] +solana-program-test.workspace = true +solana-sdk.workspace = true + +hyperlane-test-utils = { path = "../../libraries/test-utils" } +hyperlane-sealevel-test-ism = { path = "../ism/test-ism", features = [ + "no-entrypoint", +] } + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-collateral-memo/src/instruction.rs b/rust/sealevel/programs/hyperlane-sealevel-token-collateral-memo/src/instruction.rs new file mode 100644 index 00000000000..3f3d0af86da --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-collateral-memo/src/instruction.rs @@ -0,0 +1,47 @@ +//! Instructions for the program. + +use hyperlane_sealevel_token_lib::instruction::{init_instruction as lib_init_instruction, Init}; + +use crate::{hyperlane_token_ata_payer_pda_seeds, hyperlane_token_escrow_pda_seeds}; + +use solana_program::{ + instruction::{AccountMeta, Instruction as SolanaInstruction}, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, + sysvar::SysvarId, +}; + +/// Gets an instruction to initialize the program. +pub fn init_instruction( + program_id: Pubkey, + payer: Pubkey, + init: Init, + spl_program: Pubkey, + mint: Pubkey, +) -> Result { + let mut instruction = lib_init_instruction(program_id, payer, init)?; + + // Add additional account metas: + // 0. `[executable]` The SPL token program for the mint, i.e. either SPL token program or the 2022 version. + // 1. `[]` The mint. + // 2. `[executable]` The Rent sysvar program. + // 3. `[writable]` The escrow PDA account. + // 4. `[writable]` The ATA payer PDA account. + + let (escrow_key, _escrow_bump) = + Pubkey::find_program_address(hyperlane_token_escrow_pda_seeds!(), &program_id); + + let (ata_payer_key, _ata_payer_bump) = + Pubkey::find_program_address(hyperlane_token_ata_payer_pda_seeds!(), &program_id); + + instruction.accounts.append(&mut vec![ + AccountMeta::new_readonly(spl_program, false), + AccountMeta::new_readonly(mint, false), + AccountMeta::new_readonly(Rent::id(), false), + AccountMeta::new(escrow_key, false), + AccountMeta::new(ata_payer_key, false), + ]); + + Ok(instruction) +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-collateral-memo/src/lib.rs b/rust/sealevel/programs/hyperlane-sealevel-token-collateral-memo/src/lib.rs new file mode 100644 index 00000000000..ee25eb133e7 --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-collateral-memo/src/lib.rs @@ -0,0 +1,14 @@ +//! The hyperlane-sealevel-token-collateral program. + +#![deny(warnings)] +#![deny(missing_docs)] +#![deny(unsafe_code)] + +pub mod instruction; +pub mod plugin; +pub mod processor; + +pub use spl_associated_token_account; +pub use spl_noop; +pub use spl_token; +pub use spl_token_2022; diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-collateral-memo/src/plugin.rs b/rust/sealevel/programs/hyperlane-sealevel-token-collateral-memo/src/plugin.rs new file mode 100644 index 00000000000..2b97b0f1c5f --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-collateral-memo/src/plugin.rs @@ -0,0 +1,457 @@ +//! A plugin for the Hyperlane token program that escrows SPL tokens as collateral. + +use account_utils::{create_pda_account, verify_rent_exempt, SizedData}; +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_sealevel_token_lib::{ + accounts::HyperlaneToken, processor::HyperlaneSealevelTokenPlugin, +}; +use hyperlane_warp_route::TokenMessage; +use serializable_account_meta::SerializableAccountMeta; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + instruction::AccountMeta, + program::{get_return_data, invoke, invoke_signed}, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, + sysvar::{self, Sysvar}, +}; +use spl_associated_token_account::{ + get_associated_token_address_with_program_id, + instruction::create_associated_token_account_idempotent, +}; +use spl_token_2022::instruction::{get_account_data_size, initialize_account, transfer_checked}; + +/// Seeds relating to the PDA account that acts both as the mint +/// *and* the mint authority. +#[macro_export] +macro_rules! hyperlane_token_escrow_pda_seeds { + () => {{ + &[b"hyperlane_token", b"-", b"escrow"] + }}; + + ($bump_seed:expr) => {{ + &[b"hyperlane_token", b"-", b"escrow", &[$bump_seed]] + }}; +} + +/// Seeds relating to the PDA account that acts as the payer for +/// ATA creation. +#[macro_export] +macro_rules! hyperlane_token_ata_payer_pda_seeds { + () => {{ + &[b"hyperlane_token", b"-", b"ata_payer"] + }}; + + ($bump_seed:expr) => {{ + &[b"hyperlane_token", b"-", b"ata_payer", &[$bump_seed]] + }}; +} + +/// A plugin for the Hyperlane token program that escrows SPL +/// tokens when transferring out to a remote chain, and pays them +/// out when transferring in from a remote chain. +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq, Default)] +pub struct CollateralPlugin { + /// The SPL token program, i.e. either SPL token program or the 2022 version. + pub spl_token_program: Pubkey, + /// The mint. + pub mint: Pubkey, + /// The escrow PDA account. + pub escrow: Pubkey, + /// The escrow PDA bump seed. + pub escrow_bump: u8, + /// The ATA payer PDA bump seed. + pub ata_payer_bump: u8, +} + +impl SizedData for CollateralPlugin { + fn size(&self) -> usize { + // spl_token_program + 32 + // mint + + 32 + // escrow + + 32 + // escrow_bump + + std::mem::size_of::() + // ata_payer_bump + + std::mem::size_of::() + } +} + +impl CollateralPlugin { + fn verify_ata_payer_account_info( + program_id: &Pubkey, + token: &HyperlaneToken, + ata_payer_account_info: &AccountInfo, + ) -> Result<(), ProgramError> { + let ata_payer_seeds: &[&[u8]] = + hyperlane_token_ata_payer_pda_seeds!(token.plugin_data.ata_payer_bump); + let expected_ata_payer_account = + Pubkey::create_program_address(ata_payer_seeds, program_id)?; + if ata_payer_account_info.key != &expected_ata_payer_account { + return Err(ProgramError::InvalidArgument); + } + Ok(()) + } +} + +impl HyperlaneSealevelTokenPlugin for CollateralPlugin { + /// Initializes the plugin. + /// + /// Accounts: + /// 0. `[executable]` The SPL token program for the mint, i.e. either SPL token program or the 2022 version. + /// 1. `[]` The mint. + /// 2. `[executable]` The Rent sysvar program. + /// 3. `[writable]` The escrow PDA account. + /// 4. `[writable]` The ATA payer PDA account. + fn initialize<'a, 'b>( + program_id: &Pubkey, + system_program: &'a AccountInfo<'b>, + _token_account_info: &'a AccountInfo<'b>, + payer_account_info: &'a AccountInfo<'b>, + accounts_iter: &mut std::slice::Iter<'a, AccountInfo<'b>>, + ) -> Result { + // Account 0: The SPL token program. + // This can either be the original SPL token program or the 2022 version. + // This is saved in the HyperlaneToken plugin data so that future interactions + // are done with the correct SPL token program. + let spl_token_account_info = next_account_info(accounts_iter)?; + if spl_token_account_info.key != &spl_token_2022::id() + && spl_token_account_info.key != &spl_token::id() + { + return Err(ProgramError::IncorrectProgramId); + } + + // Account 1: The mint. + let mint_account_info = next_account_info(accounts_iter)?; + if mint_account_info.owner != spl_token_account_info.key { + return Err(ProgramError::IllegalOwner); + } + + // Account 2: The Rent sysvar program. + let rent_account_info = next_account_info(accounts_iter)?; + if rent_account_info.key != &sysvar::rent::id() { + return Err(ProgramError::IncorrectProgramId); + } + + // Account 3: Escrow PDA account. + let escrow_account_info = next_account_info(accounts_iter)?; + let (escrow_key, escrow_bump) = + Pubkey::find_program_address(hyperlane_token_escrow_pda_seeds!(), program_id); + if &escrow_key != escrow_account_info.key { + return Err(ProgramError::IncorrectProgramId); + } + + // Get the required account size for the escrow PDA. + invoke( + &get_account_data_size( + spl_token_account_info.key, + mint_account_info.key, + // No additional extensions + &[], + )?, + &[mint_account_info.clone()], + )?; + let account_data_size: u64 = get_return_data() + .ok_or(ProgramError::InvalidArgument) + .and_then(|(returning_pubkey, data)| { + if &returning_pubkey != spl_token_account_info.key { + return Err(ProgramError::InvalidArgument); + } + let data: [u8; 8] = data + .as_slice() + .try_into() + .map_err(|_| ProgramError::InvalidArgument)?; + Ok(u64::from_le_bytes(data)) + })?; + + let rent = Rent::get()?; + + // Create escrow PDA owned by the SPL token program. + create_pda_account( + payer_account_info, + &rent, + account_data_size.try_into().unwrap(), + spl_token_account_info.key, + system_program, + escrow_account_info, + hyperlane_token_escrow_pda_seeds!(escrow_bump), + )?; + + // And initialize the escrow account. + invoke( + &initialize_account( + spl_token_account_info.key, + escrow_account_info.key, + mint_account_info.key, + escrow_account_info.key, + )?, + &[ + escrow_account_info.clone(), + mint_account_info.clone(), + escrow_account_info.clone(), + rent_account_info.clone(), + ], + )?; + + // Account 4: ATA payer. + let ata_payer_account_info = next_account_info(accounts_iter)?; + let (ata_payer_key, ata_payer_bump) = + Pubkey::find_program_address(hyperlane_token_ata_payer_pda_seeds!(), program_id); + if &ata_payer_key != ata_payer_account_info.key { + return Err(ProgramError::IncorrectProgramId); + } + + // Create the ATA payer. + // This is a separate PDA because the ATA program requires + // the payer to have no data in it. + create_pda_account( + payer_account_info, + &rent, + 0, + // Grant ownership to the system program so that the ATA program + // can call into the system program with the ATA payer as the + // payer. + &solana_program::system_program::id(), + system_program, + ata_payer_account_info, + hyperlane_token_ata_payer_pda_seeds!(ata_payer_bump), + )?; + + Ok(Self { + spl_token_program: *spl_token_account_info.key, + mint: *mint_account_info.key, + escrow: escrow_key, + escrow_bump, + ata_payer_bump, + }) + } + + /// Transfers tokens to the escrow account so they can be sent to a remote chain. + /// Burns the tokens from the sender's associated token account. + /// + /// Accounts: + /// 0. `[executable]` The SPL token program for the mint. + /// 1. `[writeable]` The mint. + /// 2. `[writeable]` The token sender's associated token account, from which tokens will be sent. + /// 3. `[writeable]` The escrow PDA account. + fn transfer_in<'a, 'b>( + _program_id: &Pubkey, + token: &HyperlaneToken, + sender_wallet_account_info: &'a AccountInfo<'b>, + accounts_iter: &mut std::slice::Iter<'a, AccountInfo<'b>>, + amount: u64, + ) -> Result<(), ProgramError> { + // Account 0: SPL token program. + let spl_token_account_info = next_account_info(accounts_iter)?; + if spl_token_account_info.key != &token.plugin_data.spl_token_program { + return Err(ProgramError::IncorrectProgramId); + } + if !spl_token_account_info.executable { + return Err(ProgramError::InvalidAccountData); + } + + // Account 1: The mint. + let mint_account_info = next_account_info(accounts_iter)?; + if mint_account_info.key != &token.plugin_data.mint { + return Err(ProgramError::IncorrectProgramId); + } + if mint_account_info.owner != spl_token_account_info.key { + return Err(ProgramError::InvalidAccountData); + } + + // Account 2: The sender's associated token account. + let sender_ata_account_info = next_account_info(accounts_iter)?; + let expected_sender_associated_token_key = get_associated_token_address_with_program_id( + sender_wallet_account_info.key, + mint_account_info.key, + spl_token_account_info.key, + ); + if sender_ata_account_info.key != &expected_sender_associated_token_key { + return Err(ProgramError::IncorrectProgramId); + } + + // Account 3: The escrow PDA account. + let escrow_account_info = next_account_info(accounts_iter)?; + if escrow_account_info.key != &token.plugin_data.escrow { + return Err(ProgramError::IncorrectProgramId); + } + + let transfer_instruction = transfer_checked( + spl_token_account_info.key, + sender_ata_account_info.key, + mint_account_info.key, + escrow_account_info.key, + sender_wallet_account_info.key, + // Multisignatures not supported at the moment. + &[], + amount, + token.decimals, + )?; + + // Sender wallet is expected to have signed this transaction. + invoke( + &transfer_instruction, + &[ + sender_ata_account_info.clone(), + mint_account_info.clone(), + escrow_account_info.clone(), + sender_wallet_account_info.clone(), + ], + )?; + + Ok(()) + } + + /// Transfers tokens out to a recipient's associated token account as a + /// result of a transfer to this chain from a remote chain. + /// + /// Accounts: + /// 0. `[executable]` SPL token for the mint. + /// 1. `[executable]` SPL associated token account. + /// 2. `[writeable]` Mint account. + /// 3. `[writeable]` Recipient associated token account. + /// 4. `[writeable]` ATA payer PDA account. + /// 5. `[writeable]` Escrow account. + fn transfer_out<'a, 'b>( + program_id: &Pubkey, + token: &HyperlaneToken, + system_program_account_info: &'a AccountInfo<'b>, + recipient_wallet_account_info: &'a AccountInfo<'b>, + accounts_iter: &mut std::slice::Iter<'a, AccountInfo<'b>>, + amount: u64, + ) -> Result<(), ProgramError> { + // Account 0: SPL token program. + let spl_token_account_info = next_account_info(accounts_iter)?; + if spl_token_account_info.key != &token.plugin_data.spl_token_program { + return Err(ProgramError::IncorrectProgramId); + } + if !spl_token_account_info.executable { + return Err(ProgramError::InvalidAccountData); + } + + // Account 1: SPL associated token account + let spl_ata_account_info = next_account_info(accounts_iter)?; + if spl_ata_account_info.key != &spl_associated_token_account::id() { + return Err(ProgramError::IncorrectProgramId); + } + if !spl_ata_account_info.executable { + return Err(ProgramError::InvalidAccountData); + } + + // Account 2: Mint account + let mint_account_info = next_account_info(accounts_iter)?; + if mint_account_info.key != &token.plugin_data.mint { + return Err(ProgramError::IncorrectProgramId); + } + + // Account 3: Recipient associated token account + let recipient_ata_account_info = next_account_info(accounts_iter)?; + let expected_recipient_associated_token_account_key = + get_associated_token_address_with_program_id( + recipient_wallet_account_info.key, + mint_account_info.key, + spl_token_account_info.key, + ); + if recipient_ata_account_info.key != &expected_recipient_associated_token_account_key { + return Err(ProgramError::IncorrectProgramId); + } + if !recipient_ata_account_info.is_writable { + return Err(ProgramError::InvalidAccountData); + } + + // Account 4: ATA payer PDA account + let ata_payer_account_info = next_account_info(accounts_iter)?; + Self::verify_ata_payer_account_info(program_id, token, ata_payer_account_info)?; + + // Account 5: Escrow account. + let escrow_account_info = next_account_info(accounts_iter)?; + if escrow_account_info.key != &token.plugin_data.escrow { + return Err(ProgramError::IncorrectProgramId); + } + + // Create and init (this does both) associated token account if necessary. + invoke_signed( + &create_associated_token_account_idempotent( + ata_payer_account_info.key, + recipient_wallet_account_info.key, + mint_account_info.key, + spl_token_account_info.key, + ), + &[ + ata_payer_account_info.clone(), + recipient_ata_account_info.clone(), + recipient_wallet_account_info.clone(), + mint_account_info.clone(), + system_program_account_info.clone(), + spl_token_account_info.clone(), + ], + &[hyperlane_token_ata_payer_pda_seeds!( + token.plugin_data.ata_payer_bump + )], + )?; + + // After potentially paying for the ATA creation, we need to make sure + // the ATA payer still meets the rent-exemption requirements! + verify_rent_exempt(ata_payer_account_info, &Rent::get()?)?; + + let transfer_instruction = transfer_checked( + spl_token_account_info.key, + escrow_account_info.key, + mint_account_info.key, + recipient_ata_account_info.key, + escrow_account_info.key, + &[], + amount, + token.decimals, + )?; + + invoke_signed( + &transfer_instruction, + &[ + escrow_account_info.clone(), + mint_account_info.clone(), + recipient_ata_account_info.clone(), + escrow_account_info.clone(), + ], + &[hyperlane_token_escrow_pda_seeds!( + token.plugin_data.escrow_bump + )], + )?; + + Ok(()) + } + + /// Returns the accounts required for `transfer_out`. + fn transfer_out_account_metas( + program_id: &Pubkey, + token: &HyperlaneToken, + token_message: &TokenMessage, + ) -> Result<(Vec, bool), ProgramError> { + let ata_payer_account_key = Pubkey::create_program_address( + hyperlane_token_ata_payer_pda_seeds!(token.plugin_data.ata_payer_bump), + program_id, + )?; + + let recipient_associated_token_account = get_associated_token_address_with_program_id( + &Pubkey::new_from_array(token_message.recipient().into()), + &token.plugin_data.mint, + &token.plugin_data.spl_token_program, + ); + + Ok(( + vec![ + AccountMeta::new_readonly(token.plugin_data.spl_token_program, false).into(), + AccountMeta::new_readonly(spl_associated_token_account::id(), false).into(), + AccountMeta::new_readonly(token.plugin_data.mint, false).into(), + AccountMeta::new(recipient_associated_token_account, false).into(), + AccountMeta::new(ata_payer_account_key, false).into(), + AccountMeta::new(token.plugin_data.escrow, false).into(), + ], + // The recipient does not need to be writeable + false, + )) + } +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-collateral-memo/src/processor.rs b/rust/sealevel/programs/hyperlane-sealevel-token-collateral-memo/src/processor.rs new file mode 100644 index 00000000000..ba360809609 --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-collateral-memo/src/processor.rs @@ -0,0 +1,297 @@ +//! Program processor. + +use account_utils::DiscriminatorDecode; +use hyperlane_sealevel_connection_client::{ + gas_router::GasRouterConfig, router::RemoteRouterConfig, +}; +use hyperlane_sealevel_igp::accounts::InterchainGasPaymasterType; +use hyperlane_sealevel_message_recipient_interface::{ + HandleInstruction, MessageRecipientInstruction, +}; +use hyperlane_sealevel_token_lib::{ + instruction::{ + DymInstruction, Init, Instruction as TokenIxn, TransferRemote, TransferRemoteMemo, + }, + processor::HyperlaneSealevelToken, +}; +use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, msg, pubkey::Pubkey}; + +use crate::plugin::CollateralPlugin; + +#[cfg(not(feature = "no-entrypoint"))] +solana_program::entrypoint!(process_instruction); + +/// Processes an instruction. +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + // First, check if the instruction has a discriminant relating to + // the message recipient interface. + if let Ok(message_recipient_instruction) = MessageRecipientInstruction::decode(instruction_data) + { + return match message_recipient_instruction { + MessageRecipientInstruction::InterchainSecurityModule => { + interchain_security_module(program_id, accounts) + } + MessageRecipientInstruction::InterchainSecurityModuleAccountMetas => { + interchain_security_module_account_metas(program_id) + } + MessageRecipientInstruction::Handle(handle) => transfer_from_remote( + program_id, + accounts, + HandleInstruction { + origin: handle.origin, + sender: handle.sender, + message: handle.message, + }, + ), + MessageRecipientInstruction::HandleAccountMetas(handle) => { + transfer_from_remote_account_metas( + program_id, + accounts, + HandleInstruction { + origin: handle.origin, + sender: handle.sender, + message: handle.message, + }, + ) + } + }; + } + if let Ok(instr) = DymInstruction::decode(instruction_data) { + return match instr { + DymInstruction::TransferRemoteMemo(xfer) => { + transfer_remote_memo(program_id, accounts, xfer) + } + } + .map_err(|err| { + msg!("{}", err); + err + }); + } + + // Otherwise, try decoding a "normal" token instruction + match TokenIxn::decode(instruction_data)? { + TokenIxn::Init(init) => initialize(program_id, accounts, init), + TokenIxn::TransferRemote(xfer) => transfer_remote(program_id, accounts, xfer), + TokenIxn::EnrollRemoteRouter(config) => enroll_remote_router(program_id, accounts, config), + TokenIxn::EnrollRemoteRouters(configs) => { + enroll_remote_routers(program_id, accounts, configs) + } + TokenIxn::SetDestinationGasConfigs(configs) => { + set_destination_gas_configs(program_id, accounts, configs) + } + TokenIxn::TransferOwnership(new_owner) => { + transfer_ownership(program_id, accounts, new_owner) + } + TokenIxn::SetInterchainSecurityModule(new_ism) => { + set_interchain_security_module(program_id, accounts, new_ism) + } + TokenIxn::SetInterchainGasPaymaster(new_igp) => { + set_interchain_gas_paymaster(program_id, accounts, new_igp) + } + } + .map_err(|err| { + msg!("{}", err); + err + }) +} + +fn transfer_remote_memo( + program_id: &Pubkey, + accounts: &[AccountInfo], + transfer: TransferRemoteMemo, +) -> ProgramResult { + let base = transfer.base; + let memo = transfer.memo; + HyperlaneSealevelToken::::transfer_remote_memo( + program_id, accounts, base, memo, + ) +} + +/// Initializes the program. +/// +/// Accounts: +/// 0. `[executable]` The system program. +/// 1. `[writable]` The token PDA account. +/// 2. `[writable]` The dispatch authority PDA account. +/// 3. `[signer]` The payer and access control owner of the program. +/// 4. `[executable]` The SPL token program for the mint, i.e. either SPL token program or the 2022 version. +/// 5. `[]` The mint. +/// 6. `[executable]` The Rent sysvar program. +/// 7. `[writable]` The escrow PDA account. +/// 8. `[writable]` The ATA payer PDA account. +fn initialize(program_id: &Pubkey, accounts: &[AccountInfo], init: Init) -> ProgramResult { + HyperlaneSealevelToken::::initialize(program_id, accounts, init) +} + +/// Transfers tokens to a remote. +/// Transfers the collateral token into the escrow PDA account and +/// then dispatches a message to the remote recipient. +/// +/// Accounts: +/// 0. `[executable]` The system program. +/// 1. `[executable]` The spl_noop program. +/// 2. `[]` The token PDA account. +/// 3. `[executable]` The mailbox program. +/// 4. `[writeable]` The mailbox outbox account. +/// 5. `[]` Message dispatch authority. +/// 6. `[signer]` The token sender and mailbox payer. +/// 7. `[signer]` Unique message / gas payment account. +/// 8. `[writeable]` Message storage PDA. +/// ---- If using an IGP ---- +/// 9. `[executable]` The IGP program. +/// 10. `[writeable]` The IGP program data. +/// 11. `[writeable]` Gas payment PDA. +/// 12. `[]` OPTIONAL - The Overhead IGP program, if the configured IGP is an Overhead IGP. +/// 13. `[writeable]` The IGP account. +/// ---- End if ---- +/// 14. `[executable]` The SPL token program for the mint. +/// 15. `[writeable]` The mint. +/// 16. `[writeable]` The token sender's associated token account, from which tokens will be sent. +/// 17. `[writeable]` The escrow PDA account. +fn transfer_remote( + program_id: &Pubkey, + accounts: &[AccountInfo], + transfer: TransferRemote, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_remote(program_id, accounts, transfer) +} + +// Accounts: +// 0. `[signer]` Mailbox process authority specific to this program. +// 1. `[executable]` system_program +// 2. `[]` hyperlane_token storage +// 3. `[]` recipient wallet address +// 4. `[executable]` SPL token 2022 program. +// 5. `[executable]` SPL associated token account. +// 6. `[writeable]` Mint account. +// 7. `[writeable]` Recipient associated token account. +// 8. `[writeable]` ATA payer PDA account. +// 9. `[writeable]` Escrow account. +fn transfer_from_remote( + program_id: &Pubkey, + accounts: &[AccountInfo], + transfer: HandleInstruction, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_from_remote(program_id, accounts, transfer) +} + +/// Gets the account metas for a `transfer_from_remote` instruction. +/// +/// Accounts: +/// None +fn transfer_from_remote_account_metas( + program_id: &Pubkey, + accounts: &[AccountInfo], + transfer: HandleInstruction, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_from_remote_account_metas( + program_id, accounts, transfer, + ) +} + +/// Enrolls a remote router. +/// +/// Accounts: +/// 0. `[executable]` The system program. +/// 1. `[writeable]` The token PDA account. +/// 2. `[signer]` The owner. +fn enroll_remote_router( + program_id: &Pubkey, + accounts: &[AccountInfo], + config: RemoteRouterConfig, +) -> ProgramResult { + HyperlaneSealevelToken::::enroll_remote_router(program_id, accounts, config) +} + +/// Enrolls remote routers. +/// +/// Accounts: +/// 0. `[executable]` The system program. +/// 1. `[writeable]` The token PDA account. +/// 2. `[signer]` The owner. +fn enroll_remote_routers( + program_id: &Pubkey, + accounts: &[AccountInfo], + configs: Vec, +) -> ProgramResult { + HyperlaneSealevelToken::::enroll_remote_routers(program_id, accounts, configs) +} + +/// Sets the destination gas configs. +/// +/// Accounts: +/// 0. `[executable]` The system program. +/// 1. `[writeable]` The token PDA account. +/// 2. `[signer]` The owner. +fn set_destination_gas_configs( + program_id: &Pubkey, + accounts: &[AccountInfo], + configs: Vec, +) -> ProgramResult { + HyperlaneSealevelToken::::set_destination_gas_configs( + program_id, accounts, configs, + ) +} + +/// Transfers ownership. +/// +/// Accounts: +/// 0. `[writeable]` The token PDA account. +/// 1. `[signer]` The current owner. +fn transfer_ownership( + program_id: &Pubkey, + accounts: &[AccountInfo], + new_owner: Option, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_ownership(program_id, accounts, new_owner) +} + +/// Gets the interchain security module, returning it as a serialized Option. +/// +/// Accounts: +/// 0. `[]` The token PDA account. +fn interchain_security_module(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + HyperlaneSealevelToken::::interchain_security_module(program_id, accounts) +} + +/// Gets the account metas for getting the interchain security module. +/// +/// Accounts: +/// None +fn interchain_security_module_account_metas(program_id: &Pubkey) -> ProgramResult { + HyperlaneSealevelToken::::interchain_security_module_account_metas(program_id) +} + +/// Lets the owner set the interchain security module. +/// +/// Accounts: +/// 0. `[writeable]` The token PDA account. +/// 1. `[signer]` The access control owner. +fn set_interchain_security_module( + program_id: &Pubkey, + accounts: &[AccountInfo], + new_ism: Option, +) -> ProgramResult { + HyperlaneSealevelToken::::set_interchain_security_module( + program_id, accounts, new_ism, + ) +} + +/// Lets the owner set the interchain gas paymaster. +/// +/// Accounts: +/// 0. `[writeable]` The token PDA account. +/// 1. `[signer]` The access control owner. +fn set_interchain_gas_paymaster( + program_id: &Pubkey, + accounts: &[AccountInfo], + new_igp: Option<(Pubkey, InterchainGasPaymasterType)>, +) -> ProgramResult { + HyperlaneSealevelToken::::set_interchain_gas_paymaster( + program_id, accounts, new_igp, + ) +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-collateral-memo/tests/functional.rs b/rust/sealevel/programs/hyperlane-sealevel-token-collateral-memo/tests/functional.rs new file mode 100644 index 00000000000..a2ad7efeb00 --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-collateral-memo/tests/functional.rs @@ -0,0 +1,1809 @@ +//! Contains functional tests for things that cannot be done +//! strictly in unit tests. This includes CPIs, like creating +//! new PDA accounts. + +use account_utils::DiscriminatorEncode; +use hyperlane_core::{Encode, HyperlaneMessage, H256, U256}; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + program_pack::Pack, + pubkey, + pubkey::Pubkey, + rent::Rent, + system_instruction, +}; +use std::collections::HashMap; + +use hyperlane_sealevel_connection_client::{ + gas_router::GasRouterConfig, router::RemoteRouterConfig, +}; +use hyperlane_sealevel_igp::{ + accounts::{GasPaymentAccount, GasPaymentData, InterchainGasPaymasterType}, + igp_gas_payment_pda_seeds, +}; +use hyperlane_sealevel_mailbox::{ + accounts::{DispatchedMessage, DispatchedMessageAccount}, + mailbox_dispatched_message_pda_seeds, mailbox_message_dispatch_authority_pda_seeds, + mailbox_process_authority_pda_seeds, + protocol_fee::ProtocolFee, +}; +use hyperlane_sealevel_message_recipient_interface::{ + HandleInstruction, MessageRecipientInstruction, +}; +use hyperlane_sealevel_token_collateral::{ + hyperlane_token_ata_payer_pda_seeds, hyperlane_token_escrow_pda_seeds, plugin::CollateralPlugin, +}; +use hyperlane_sealevel_token_collateral_memo::processor::process_instruction; +use hyperlane_sealevel_token_lib::{ + accounts::{convert_decimals, HyperlaneToken, HyperlaneTokenAccount}, + hyperlane_token_pda_seeds, + instruction::{ + DymInstruction, Init, Instruction as HyperlaneTokenInstruction, TransferRemote, + TransferRemoteMemo, + }, +}; +use hyperlane_test_utils::{ + assert_token_balance, assert_transaction_error, igp_program_id, initialize_igp_accounts, + initialize_mailbox, mailbox_id, new_funded_keypair, process, transfer_lamports, IgpAccounts, +}; +use hyperlane_warp_route::TokenMessage; +use solana_program_test::*; +use solana_sdk::{ + instruction::InstructionError, + signature::Signer, + signer::keypair::Keypair, + transaction::{Transaction, TransactionError}, +}; +use spl_associated_token_account::instruction::create_associated_token_account_idempotent; +use spl_token_2022::instruction::initialize_mint2; + +/// There are 1e9 lamports in one SOL. +const ONE_SOL_IN_LAMPORTS: u64 = 1000000000; +const LOCAL_DOMAIN: u32 = 1234; +const LOCAL_DECIMALS: u8 = 8; +const LOCAL_DECIMALS_U32: u32 = LOCAL_DECIMALS as u32; +const REMOTE_DOMAIN: u32 = 4321; +const REMOTE_DECIMALS: u8 = 18; +const REMOTE_GAS_AMOUNT: u64 = 200000; +// Same for spl_token_2022 and spl_token +const MINT_ACCOUNT_LEN: usize = spl_token_2022::state::Mint::LEN; + +fn hyperlane_sealevel_token_collateral_id() -> Pubkey { + pubkey!("G8t1qe3YnYvhi1zS9ioUXuVFkwhBgvfHaLJt5X6PF18z") +} + +async fn setup_client() -> (BanksClient, Keypair) { + let program_id = hyperlane_sealevel_token_collateral_id(); + let mut program_test = ProgramTest::new( + "hyperlane_sealevel_token_collateral", + program_id, + processor!(process_instruction), + ); + + program_test.add_program( + "spl_token_2022", + spl_token_2022::id(), + processor!(spl_token_2022::processor::Processor::process), + ); + + program_test.add_program( + "spl_token", + spl_token::id(), + processor!(spl_token::processor::Processor::process), + ); + + program_test.add_program( + "spl_associated_token_account", + spl_associated_token_account::id(), + processor!(spl_associated_token_account::processor::process_instruction), + ); + + program_test.add_program("spl_noop", spl_noop::id(), processor!(spl_noop::noop)); + + let mailbox_program_id = mailbox_id(); + program_test.add_program( + "hyperlane_sealevel_mailbox", + mailbox_program_id, + processor!(hyperlane_sealevel_mailbox::processor::process_instruction), + ); + + program_test.add_program( + "hyperlane_sealevel_igp", + igp_program_id(), + processor!(hyperlane_sealevel_igp::processor::process_instruction), + ); + + // This serves as the default ISM on the Mailbox + program_test.add_program( + "hyperlane_sealevel_test_ism", + hyperlane_sealevel_test_ism::id(), + processor!(hyperlane_sealevel_test_ism::program::process_instruction), + ); + + let (banks_client, payer, _recent_blockhash) = program_test.start().await; + + (banks_client, payer) +} + +async fn initialize_mint( + banks_client: &mut BanksClient, + payer: &Keypair, + decimals: u8, + spl_token_program: &Pubkey, +) -> (Pubkey, Keypair) { + let mint = Keypair::new(); + let mint_authority = new_funded_keypair(banks_client, payer, ONE_SOL_IN_LAMPORTS).await; + + let payer_pubkey = payer.pubkey(); + let mint_pubkey = mint.pubkey(); + let mint_authority_pubkey = mint_authority.pubkey(); + + let init_mint_instruction = initialize_mint2( + spl_token_program, + &mint_pubkey, + &mint_authority_pubkey, + // No freeze authority + None, + decimals, + ) + .unwrap(); + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::create_account( + &payer_pubkey, + &mint_pubkey, + Rent::default().minimum_balance(MINT_ACCOUNT_LEN), + MINT_ACCOUNT_LEN.try_into().unwrap(), + spl_token_program, + ), + init_mint_instruction, + ], + Some(&payer_pubkey), + &[payer, &mint], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + (mint_pubkey, mint_authority) +} + +async fn mint_to( + banks_client: &mut BanksClient, + spl_token_program_id: &Pubkey, + mint: &Pubkey, + mint_authority: &Keypair, + recipient_account: &Pubkey, + amount: u64, +) { + let mint_instruction = spl_token_2022::instruction::mint_to( + spl_token_program_id, + mint, + recipient_account, + &mint_authority.pubkey(), + &[], + amount, + ) + .unwrap(); + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[mint_instruction], + Some(&mint_authority.pubkey()), + &[mint_authority], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); +} + +async fn create_and_mint_to_ata( + banks_client: &mut BanksClient, + spl_token_program_id: &Pubkey, + mint: &Pubkey, + mint_authority: &Keypair, + payer: &Keypair, + recipient_wallet: &Pubkey, + amount: u64, +) -> Pubkey { + let recipient_associated_token_account = + spl_associated_token_account::get_associated_token_address_with_program_id( + recipient_wallet, + mint, + spl_token_program_id, + ); + + // Create and init (this does both) associated token account if necessary. + let create_ata_instruction = create_associated_token_account_idempotent( + &payer.pubkey(), + recipient_wallet, + mint, + spl_token_program_id, + ); + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[create_ata_instruction], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + // Mint tokens to the associated token account. + if amount > 0 { + mint_to( + banks_client, + spl_token_program_id, + mint, + mint_authority, + &recipient_associated_token_account, + amount, + ) + .await; + } + + recipient_associated_token_account +} + +struct HyperlaneTokenAccounts { + token: Pubkey, + token_bump: u8, + mailbox_process_authority: Pubkey, + dispatch_authority: Pubkey, + dispatch_authority_bump: u8, + escrow: Pubkey, + escrow_bump: u8, + ata_payer: Pubkey, + ata_payer_bump: u8, +} + +async fn initialize_hyperlane_token( + program_id: &Pubkey, + banks_client: &mut BanksClient, + payer: &Keypair, + igp_accounts: Option<&IgpAccounts>, + mint: &Pubkey, + spl_token_program: &Pubkey, +) -> Result { + let (mailbox_process_authority_key, _mailbox_process_authority_bump) = + Pubkey::find_program_address( + mailbox_process_authority_pda_seeds!(program_id), + &mailbox_id(), + ); + + let (token_account_key, token_account_bump_seed) = + Pubkey::find_program_address(hyperlane_token_pda_seeds!(), program_id); + + let (dispatch_authority_key, dispatch_authority_seed) = + Pubkey::find_program_address(mailbox_message_dispatch_authority_pda_seeds!(), program_id); + + let (escrow_account_key, escrow_account_bump_seed) = + Pubkey::find_program_address(hyperlane_token_escrow_pda_seeds!(), program_id); + + let (ata_payer_account_key, ata_payer_account_bump_seed) = + Pubkey::find_program_address(hyperlane_token_ata_payer_pda_seeds!(), program_id); + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + *program_id, + &HyperlaneTokenInstruction::Init(Init { + mailbox: mailbox_id(), + interchain_security_module: None, + interchain_gas_paymaster: igp_accounts.map(|igp_accounts| { + ( + igp_accounts.program, + InterchainGasPaymasterType::OverheadIgp(igp_accounts.overhead_igp), + ) + }), + decimals: LOCAL_DECIMALS, + remote_decimals: REMOTE_DECIMALS, + }) + .encode() + .unwrap(), + vec![ + // 0. `[executable]` The system program. + // 1. `[writable]` The token PDA account. + // 2. `[writable]` The dispatch authority PDA account. + // 3. `[signer]` The payer. + // 4. `[executable]` The SPL token program for the mint, i.e. either SPL token program or the 2022 version. + // 5. `[]` The mint. + // 6. `[executable]` The Rent sysvar program. + // 7. `[writable]` The escrow PDA account. + // 8. `[writable]` The ATA payer PDA account. + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(token_account_key, false), + AccountMeta::new(dispatch_authority_key, false), + AccountMeta::new_readonly(payer.pubkey(), true), + AccountMeta::new_readonly(*spl_token_program, false), + AccountMeta::new(*mint, false), + AccountMeta::new_readonly(solana_program::sysvar::rent::id(), false), + AccountMeta::new(escrow_account_key, false), + AccountMeta::new(ata_payer_account_key, false), + ], + )], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await?; + + // Set destination gas configs + set_destination_gas_config( + banks_client, + program_id, + payer, + &token_account_key, + REMOTE_DOMAIN, + REMOTE_GAS_AMOUNT, + ) + .await?; + + Ok(HyperlaneTokenAccounts { + token: token_account_key, + token_bump: token_account_bump_seed, + mailbox_process_authority: mailbox_process_authority_key, + dispatch_authority: dispatch_authority_key, + dispatch_authority_bump: dispatch_authority_seed, + escrow: escrow_account_key, + escrow_bump: escrow_account_bump_seed, + ata_payer: ata_payer_account_key, + ata_payer_bump: ata_payer_account_bump_seed, + }) +} + +async fn enroll_remote_router( + banks_client: &mut BanksClient, + program_id: &Pubkey, + payer: &Keypair, + token_account: &Pubkey, + domain: u32, + router: H256, +) -> Result<(), BanksClientError> { + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Enroll the remote router + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + *program_id, + &HyperlaneTokenInstruction::EnrollRemoteRouter(RemoteRouterConfig { + domain, + router: Some(router), + }) + .encode() + .unwrap(), + vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(*token_account, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await?; + + Ok(()) +} + +async fn set_destination_gas_config( + banks_client: &mut BanksClient, + program_id: &Pubkey, + payer: &Keypair, + token_account: &Pubkey, + domain: u32, + gas: u64, +) -> Result<(), BanksClientError> { + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Enroll the remote router + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + *program_id, + &HyperlaneTokenInstruction::SetDestinationGasConfigs(vec![GasRouterConfig { + domain, + gas: Some(gas), + }]) + .encode() + .unwrap(), + vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(*token_account, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await?; + + Ok(()) +} + +#[tokio::test] +async fn test_initialize() { + let program_id = hyperlane_sealevel_token_collateral_id(); + let mailbox_program_id = mailbox_id(); + let spl_token_program_id = spl_token_2022::id(); + + let (mut banks_client, payer) = setup_client().await; + + let mailbox_accounts = initialize_mailbox( + &mut banks_client, + &mailbox_program_id, + &payer, + LOCAL_DOMAIN, + ONE_SOL_IN_LAMPORTS, + ProtocolFee::default(), + ) + .await + .unwrap(); + + let igp_accounts = + initialize_igp_accounts(&mut banks_client, &igp_program_id(), &payer, REMOTE_DOMAIN) + .await + .unwrap(); + + let (mint, _mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + Some(&igp_accounts), + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + // Get the token account. + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!( + token, + Box::new(HyperlaneToken { + bump: hyperlane_token_accounts.token_bump, + mailbox: mailbox_accounts.program, + mailbox_process_authority: hyperlane_token_accounts.mailbox_process_authority, + dispatch_authority_bump: hyperlane_token_accounts.dispatch_authority_bump, + decimals: LOCAL_DECIMALS, + remote_decimals: REMOTE_DECIMALS, + owner: Some(payer.pubkey()), + interchain_security_module: None, + interchain_gas_paymaster: Some(( + igp_accounts.program, + InterchainGasPaymasterType::OverheadIgp(igp_accounts.overhead_igp), + )), + destination_gas: HashMap::from([(REMOTE_DOMAIN, REMOTE_GAS_AMOUNT)]), + remote_routers: HashMap::new(), + plugin_data: CollateralPlugin { + spl_token_program: spl_token_2022::id(), + mint, + escrow: hyperlane_token_accounts.escrow, + escrow_bump: hyperlane_token_accounts.escrow_bump, + ata_payer_bump: hyperlane_token_accounts.ata_payer_bump, + }, + }), + ); + + // Verify the escrow account was created. + let escrow_account = banks_client + .get_account(hyperlane_token_accounts.escrow) + .await + .unwrap() + .unwrap(); + assert_eq!(escrow_account.owner, spl_token_2022::id()); + assert!(!escrow_account.data.is_empty()); + + // Verify the ATA payer account was created. + let ata_payer_account = banks_client + .get_account(hyperlane_token_accounts.ata_payer) + .await + .unwrap() + .unwrap(); + assert!(ata_payer_account.lamports > 0); +} + +#[tokio::test] +async fn test_initialize_errors_if_called_twice() { + let program_id = hyperlane_sealevel_token_collateral_id(); + let spl_token_program_id = spl_token_2022::id(); + + let (mut banks_client, payer) = setup_client().await; + + let (mint, mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + None, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + // To ensure a different signature is used, we'll use a different payer + let init_result = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &mint_authority, + None, + &mint, + &spl_token_program_id, + ) + .await; + + assert_transaction_error( + init_result, + TransactionError::InstructionError(0, InstructionError::AccountAlreadyInitialized), + ); +} + +async fn test_transfer_remote_memo(spl_token_program_id: Pubkey) { + let program_id = hyperlane_sealevel_token_collateral_id(); + let mailbox_program_id = mailbox_id(); + + let (mut banks_client, payer) = setup_client().await; + + let mailbox_accounts = initialize_mailbox( + &mut banks_client, + &mailbox_program_id, + &payer, + LOCAL_DOMAIN, + ONE_SOL_IN_LAMPORTS, + ProtocolFee::default(), + ) + .await + .unwrap(); + + let igp_accounts = + initialize_igp_accounts(&mut banks_client, &igp_program_id(), &payer, REMOTE_DOMAIN) + .await + .unwrap(); + + let (mint, mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + Some(&igp_accounts), + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + let token_sender = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + let token_sender_pubkey = token_sender.pubkey(); + // Mint 100 tokens to the token sender's ATA + let token_sender_ata = create_and_mint_to_ata( + &mut banks_client, + &spl_token_program_id, + &mint, + &mint_authority, + &payer, + &token_sender_pubkey, + 100 * 10u64.pow(LOCAL_DECIMALS_U32), + ) + .await; + + // Call transfer_remote + let unique_message_account_keypair = Keypair::new(); + let (dispatched_message_key, _dispatched_message_bump) = Pubkey::find_program_address( + mailbox_dispatched_message_pda_seeds!(&unique_message_account_keypair.pubkey()), + &mailbox_program_id, + ); + let (gas_payment_pda_key, _gas_payment_pda_bump) = Pubkey::find_program_address( + igp_gas_payment_pda_seeds!(&unique_message_account_keypair.pubkey()), + &igp_program_id(), + ); + + let remote_token_recipient = H256::random(); + // Transfer 69 tokens. + let transfer_amount = 69 * 10u64.pow(LOCAL_DECIMALS_U32); + let remote_transfer_amount = + convert_decimals(transfer_amount.into(), LOCAL_DECIMALS, REMOTE_DECIMALS).unwrap(); + + let test_memo = vec![1, 2, 3]; + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &DymInstruction::TransferRemoteMemo(TransferRemoteMemo { + base: TransferRemote { + destination_domain: REMOTE_DOMAIN, + recipient: remote_token_recipient, + amount_or_id: transfer_amount.into(), + }, + memo: test_memo.clone(), + }) + .encode() + .unwrap(), + // 0. `[executable]` The system program. + // 1. `[executable]` The spl_noop program. + // 2. `[]` The token PDA account. + // 3. `[executable]` The mailbox program. + // 4. `[writeable]` The mailbox outbox account. + // 5. `[]` Message dispatch authority. + // 6. `[signer]` The token sender and mailbox payer. + // 7. `[signer]` Unique message account. + // 8. `[writeable]` Message storage PDA. + // ---- If using an IGP ---- + // 9. `[executable]` The IGP program. + // 10. `[writeable]` The IGP program data. + // 11. `[writeable]` Gas payment PDA. + // 12. `[]` OPTIONAL - The Overhead IGP program, if the configured IGP is an Overhead IGP. + // 13. `[writeable]` The IGP account. + // ---- End if ---- + // 14. `[executable]` The spl_token_2022 program. + // 15. `[writeable]` The mint. + // 16. `[writeable]` The token sender's associated token account, from which tokens will be sent. + // 17. `[writeable]` The escrow PDA account. + vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new_readonly(spl_noop::id(), false), + AccountMeta::new_readonly(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(mailbox_accounts.program, false), + AccountMeta::new(mailbox_accounts.outbox, false), + AccountMeta::new_readonly(hyperlane_token_accounts.dispatch_authority, false), + AccountMeta::new_readonly(token_sender_pubkey, true), + AccountMeta::new_readonly(unique_message_account_keypair.pubkey(), true), + AccountMeta::new(dispatched_message_key, false), + AccountMeta::new_readonly(igp_accounts.program, false), + AccountMeta::new(igp_accounts.program_data, false), + AccountMeta::new(gas_payment_pda_key, false), + AccountMeta::new_readonly(igp_accounts.overhead_igp, false), + AccountMeta::new(igp_accounts.igp, false), + AccountMeta::new_readonly(spl_token_2022::id(), false), + AccountMeta::new(mint, false), + AccountMeta::new(token_sender_ata, false), + AccountMeta::new(hyperlane_token_accounts.escrow, false), + ], + )], + Some(&token_sender_pubkey), + &[&token_sender, &unique_message_account_keypair], + recent_blockhash, + ); + let tx_signature = transaction.signatures[0]; + banks_client.process_transaction(transaction).await.unwrap(); + + // Verify the token sender's ATA balance is 31 full tokens. + assert_token_balance( + &mut banks_client, + &token_sender_ata, + 31 * 10u64.pow(LOCAL_DECIMALS_U32), + ) + .await; + + // And that the escrow's balance is 69 tokens. + assert_token_balance( + &mut banks_client, + &hyperlane_token_accounts.escrow, + 69 * 10u64.pow(LOCAL_DECIMALS_U32), + ) + .await; + + // And let's take a look at the dispatched message account data to verify the message looks right. + let dispatched_message_account_data = banks_client + .get_account(dispatched_message_key) + .await + .unwrap() + .unwrap() + .data; + let dispatched_message = + DispatchedMessageAccount::fetch(&mut &dispatched_message_account_data[..]) + .unwrap() + .into_inner(); + + let transfer_remote_tx_status = banks_client + .get_transaction_status(tx_signature) + .await + .unwrap() + .unwrap(); + + let message = HyperlaneMessage { + version: 3, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: program_id.to_bytes().into(), + destination: REMOTE_DOMAIN, + recipient: remote_router, + // Expect the remote_transfer_amount to be in the message. + body: TokenMessage::new(remote_token_recipient, remote_transfer_amount, test_memo).to_vec(), + }; + + assert_eq!( + dispatched_message, + Box::new(DispatchedMessage::new( + message.nonce, + transfer_remote_tx_status.slot, + unique_message_account_keypair.pubkey(), + message.to_vec(), + )), + ); + + // And let's also look at the gas payment account to verify the gas payment looks right. + let gas_payment_account_data = banks_client + .get_account(gas_payment_pda_key) + .await + .unwrap() + .unwrap() + .data; + let gas_payment = GasPaymentAccount::fetch(&mut &gas_payment_account_data[..]) + .unwrap() + .into_inner(); + + assert_eq!( + *gas_payment, + GasPaymentData { + sequence_number: 0, + igp: igp_accounts.igp, + destination_domain: REMOTE_DOMAIN, + message_id: message.id(), + gas_amount: REMOTE_GAS_AMOUNT, + unique_gas_payment_pubkey: unique_message_account_keypair.pubkey(), + slot: transfer_remote_tx_status.slot, + payment: REMOTE_GAS_AMOUNT + } + .into(), + ); +} + +// Test transfer_remote with spl_token +#[tokio::test] +async fn test_transfer_remote_spl_token() { + test_transfer_remote_memo(spl_token_2022::id()).await; +} + +// Test transfer_remote with spl_token_2022 +#[tokio::test] +async fn test_transfer_remote_spl_token_2022() { + test_transfer_remote_memo(spl_token_2022::id()).await; +} + +async fn transfer_from_remote( + initial_escrow_balance: u64, + remote_transfer_amount: U256, + sender_override: Option, + origin_override: Option, + spl_token_program_id: Pubkey, +) -> Result<(BanksClient, HyperlaneTokenAccounts, Pubkey), BanksClientError> { + let program_id = hyperlane_sealevel_token_collateral_id(); + let mailbox_program_id = mailbox_id(); + + let (mut banks_client, payer) = setup_client().await; + + let mailbox_accounts = initialize_mailbox( + &mut banks_client, + &mailbox_program_id, + &payer, + LOCAL_DOMAIN, + ONE_SOL_IN_LAMPORTS, + ProtocolFee::default(), + ) + .await + .unwrap(); + + let igp_accounts = + initialize_igp_accounts(&mut banks_client, &igp_program_id(), &payer, REMOTE_DOMAIN) + .await + .unwrap(); + + let (mint, mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + Some(&igp_accounts), + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + // ATA payer must have a balance to create new ATAs + transfer_lamports( + &mut banks_client, + &payer, + &hyperlane_token_accounts.ata_payer, + ONE_SOL_IN_LAMPORTS, + ) + .await; + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + // Give an initial balance to the escrow account which will be used by the + // transfer_from_remote. + mint_to( + &mut banks_client, + &spl_token_program_id, + &mint, + &mint_authority, + &hyperlane_token_accounts.escrow, + initial_escrow_balance, + ) + .await; + + let recipient_pubkey = Pubkey::new_unique(); + let recipient: H256 = recipient_pubkey.to_bytes().into(); + + let recipient_associated_token_account = + spl_associated_token_account::get_associated_token_address_with_program_id( + &recipient_pubkey, + &mint, + &spl_token_program_id, + ); + + let message = HyperlaneMessage { + version: 3, + nonce: 0, + origin: origin_override.unwrap_or(REMOTE_DOMAIN), + // Default to the remote router as the sender + sender: sender_override.unwrap_or(remote_router), + destination: LOCAL_DOMAIN, + recipient: program_id.to_bytes().into(), + body: TokenMessage::new(recipient, remote_transfer_amount, vec![]).to_vec(), + }; + + process( + &mut banks_client, + &payer, + &mailbox_accounts, + vec![], + &message, + ) + .await?; + + Ok(( + banks_client, + hyperlane_token_accounts, + recipient_associated_token_account, + )) +} + +// Tests when the SPL token is the non-2022 version +#[tokio::test] +async fn test_transfer_from_remote_spl_token() { + let initial_escrow_balance = 100 * 10u64.pow(LOCAL_DECIMALS_U32); + let local_transfer_amount = 69 * 10u64.pow(LOCAL_DECIMALS_U32); + let remote_transfer_amount = convert_decimals( + local_transfer_amount.into(), + LOCAL_DECIMALS, + REMOTE_DECIMALS, + ) + .unwrap(); + + let (mut banks_client, hyperlane_token_accounts, recipient_associated_token_account) = + transfer_from_remote( + initial_escrow_balance, + remote_transfer_amount, + None, + None, + spl_token::id(), + ) + .await + .unwrap(); + + // Check that the recipient's ATA got the tokens! + assert_token_balance( + &mut banks_client, + &recipient_associated_token_account, + local_transfer_amount, + ) + .await; + + // And that the escrow's balance is lower because it was spent in the transfer. + assert_token_balance( + &mut banks_client, + &hyperlane_token_accounts.escrow, + initial_escrow_balance - local_transfer_amount, + ) + .await; +} + +// Tests when the SPL token is the 2022 version +#[tokio::test] +async fn test_transfer_from_remote_spl_token_2022() { + let initial_escrow_balance = 100 * 10u64.pow(LOCAL_DECIMALS_U32); + let local_transfer_amount = 69 * 10u64.pow(LOCAL_DECIMALS_U32); + let remote_transfer_amount = convert_decimals( + local_transfer_amount.into(), + LOCAL_DECIMALS, + REMOTE_DECIMALS, + ) + .unwrap(); + + let (mut banks_client, hyperlane_token_accounts, recipient_associated_token_account) = + transfer_from_remote( + initial_escrow_balance, + remote_transfer_amount, + None, + None, + spl_token_2022::id(), + ) + .await + .unwrap(); + + // Check that the recipient's ATA got the tokens! + assert_token_balance( + &mut banks_client, + &recipient_associated_token_account, + local_transfer_amount, + ) + .await; + + // And that the escrow's balance is lower because it was spent in the transfer. + assert_token_balance( + &mut banks_client, + &hyperlane_token_accounts.escrow, + initial_escrow_balance - local_transfer_amount, + ) + .await; +} + +#[tokio::test] +async fn test_transfer_from_remote_errors_if_sender_not_router() { + let initial_escrow_balance = 100 * 10u64.pow(LOCAL_DECIMALS_U32); + let local_transfer_amount = 69 * 10u64.pow(LOCAL_DECIMALS_U32); + let remote_transfer_amount = convert_decimals( + local_transfer_amount.into(), + LOCAL_DECIMALS, + REMOTE_DECIMALS, + ) + .unwrap(); + + // Same remote domain origin, but wrong sender. + let result = transfer_from_remote( + initial_escrow_balance, + remote_transfer_amount, + Some(H256::random()), + None, + spl_token_2022::id(), + ) + .await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidInstructionData), + ); + + // Wrong remote domain origin, but correct sender. + let result = transfer_from_remote( + initial_escrow_balance, + remote_transfer_amount, + None, + Some(REMOTE_DOMAIN + 1), + spl_token_2022::id(), + ) + .await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidInstructionData), + ); +} + +#[tokio::test] +async fn test_transfer_from_remote_errors_if_process_authority_not_signer() { + let program_id = hyperlane_sealevel_token_collateral_id(); + let mailbox_program_id = mailbox_id(); + let spl_token_program_id = spl_token_2022::id(); + + let (mut banks_client, payer) = setup_client().await; + + let _mailbox_accounts = initialize_mailbox( + &mut banks_client, + &mailbox_program_id, + &payer, + LOCAL_DOMAIN, + ONE_SOL_IN_LAMPORTS, + ProtocolFee::default(), + ) + .await + .unwrap(); + + let (mint, _mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + None, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + let recipient_pubkey = Pubkey::new_unique(); + let recipient: H256 = recipient_pubkey.to_bytes().into(); + + let recipient_associated_token_account = + spl_associated_token_account::get_associated_token_address_with_program_id( + &recipient_pubkey, + &mint, + &spl_token_2022::id(), + ); + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Try calling directly into the message handler, skipping the mailbox. + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &MessageRecipientInstruction::Handle(HandleInstruction { + origin: REMOTE_DOMAIN, + sender: remote_router, + message: TokenMessage::new(recipient, 12345u64.into(), vec![]).to_vec(), + }) + .encode() + .unwrap(), + vec![ + // Recipient.handle accounts + // 0. `[signer]` Mailbox process authority + // 1. `[executable]` system_program + // 2. `[]` hyperlane_token storage + // 3. `[]` recipient wallet address + // 4. `[executable]` SPL token 2022 program. + // 5. `[executable]` SPL associated token account. + // 6. `[writeable]` Mint account. + // 7. `[writeable]` Recipient associated token account. + // 8. `[writeable]` ATA payer PDA account. + // 9. `[writeable]` Escrow account. + AccountMeta::new_readonly( + hyperlane_token_accounts.mailbox_process_authority, + false, + ), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new_readonly(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(recipient_pubkey, false), + AccountMeta::new_readonly(spl_token_2022::id(), false), + AccountMeta::new_readonly(spl_associated_token_account::id(), false), + AccountMeta::new(mint, false), + AccountMeta::new(recipient_associated_token_account, false), + AccountMeta::new(hyperlane_token_accounts.ata_payer, false), + AccountMeta::new(hyperlane_token_accounts.escrow, false), + ], + )], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature), + ); +} + +#[tokio::test] +async fn test_enroll_remote_router() { + let program_id = hyperlane_sealevel_token_collateral_id(); + let spl_token_program_id = spl_token_2022::id(); + + let (mut banks_client, payer) = setup_client().await; + + let (mint, _mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + None, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + // Verify the remote router was enrolled. + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!( + token.remote_routers, + vec![(REMOTE_DOMAIN, remote_router)].into_iter().collect(), + ); +} + +#[tokio::test] +async fn test_enroll_remote_router_errors_if_not_signed_by_owner() { + let program_id = hyperlane_sealevel_token_collateral_id(); + let spl_token_program_id = spl_token_2022::id(); + + let (mut banks_client, payer) = setup_client().await; + + let (mint, mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + None, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + // Use the mint authority as the payer, which has a balance but is not the owner, + // so we expect this to fail. + let result = enroll_remote_router( + &mut banks_client, + &program_id, + &mint_authority, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + H256::random(), + ) + .await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); + + // Also try using the mint authority as the payer and specifying the correct + // owner account, but the owner isn't a signer: + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Enroll the remote router + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::EnrollRemoteRouter(RemoteRouterConfig { + domain: REMOTE_DOMAIN, + router: Some(H256::random()), + }) + .encode() + .unwrap(), + vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), false), + ], + )], + Some(&mint_authority.pubkey()), + &[&mint_authority], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature), + ); +} + +#[tokio::test] +async fn test_set_destination_gas_configs() { + let program_id = hyperlane_sealevel_token_collateral_id(); + let spl_token_program_id = spl_token_2022::id(); + + let (mut banks_client, payer) = setup_client().await; + + let (mint, _mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + None, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + // Set the destination gas config + let gas = 111222333; + set_destination_gas_config( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + gas, + ) + .await + .unwrap(); + + // Verify the destination gas was set. + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!( + token.destination_gas, + vec![(REMOTE_DOMAIN, gas)].into_iter().collect(), + ); +} + +#[tokio::test] +async fn test_set_destination_gas_configs_errors_if_not_signed_by_owner() { + let program_id = hyperlane_sealevel_token_collateral_id(); + let spl_token_program_id = spl_token_2022::id(); + + let (mut banks_client, payer) = setup_client().await; + + let (mint, _mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + None, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + let non_owner = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + + // Use the non_owner as the payer, which has a balance but is not the owner, + // so we expect this to fail. + let gas = 111222333; + let result = set_destination_gas_config( + &mut banks_client, + &program_id, + &non_owner, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + gas, + ) + .await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); + + // Also try using the non_owner as the payer and specifying the correct + // owner account, but the owner isn't a signer: + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Try setting + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetDestinationGasConfigs(vec![GasRouterConfig { + domain: REMOTE_DOMAIN, + gas: Some(gas), + }]) + .encode() + .unwrap(), + vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), false), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature), + ); +} + +#[tokio::test] +async fn test_transfer_ownership() { + let program_id = hyperlane_sealevel_token_collateral_id(); + let spl_token_program_id = spl_token_2022::id(); + + let (mut banks_client, payer) = setup_client().await; + + let (mint, _mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + None, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + let new_owner = Some(Pubkey::new_unique()); + + // Transfer ownership + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::TransferOwnership(new_owner) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + // Verify the new owner is set + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!(token.owner, new_owner); +} + +#[tokio::test] +async fn test_transfer_ownership_errors_if_owner_not_signer() { + let program_id = hyperlane_sealevel_token_collateral_id(); + let spl_token_program_id = spl_token_2022::id(); + + let (mut banks_client, payer) = setup_client().await; + + let (mint, mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + None, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + let new_owner = Some(Pubkey::new_unique()); + + // Try transferring ownership using the mint authority key + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::TransferOwnership(new_owner) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(mint_authority.pubkey(), true), + ], + )], + Some(&mint_authority.pubkey()), + &[&mint_authority], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); +} + +#[tokio::test] +async fn test_set_interchain_security_module() { + let program_id = hyperlane_sealevel_token_collateral_id(); + let spl_token_program_id = spl_token_2022::id(); + + let (mut banks_client, payer) = setup_client().await; + + let (mint, _mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + None, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + let new_ism = Some(Pubkey::new_unique()); + + // Set the ISM + // Transfer ownership + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainSecurityModule(new_ism) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + // Verify the new ISM is set + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!(token.interchain_security_module, new_ism); +} + +#[tokio::test] +async fn test_set_interchain_security_module_errors_if_owner_not_signer() { + let program_id = hyperlane_sealevel_token_collateral_id(); + let spl_token_program_id = spl_token_2022::id(); + + let (mut banks_client, payer) = setup_client().await; + + let (mint, mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + None, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + let new_ism = Some(Pubkey::new_unique()); + + // Try setting the ISM using the mint authority key + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainSecurityModule(new_ism) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(mint_authority.pubkey(), true), + ], + )], + Some(&mint_authority.pubkey()), + &[&mint_authority], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); + + // Also try using the non_owner as the payer and specifying the correct + // owner account, but the owner isn't a signer: + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainSecurityModule(new_ism) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), false), + ], + )], + Some(&mint_authority.pubkey()), + &[&mint_authority], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature), + ); +} + +#[tokio::test] +async fn test_set_interchain_gas_paymaster() { + let program_id = hyperlane_sealevel_token_collateral_id(); + let spl_token_program_id = spl_token_2022::id(); + + let (mut banks_client, payer) = setup_client().await; + + let (mint, _mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + None, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + let new_igp = Some(( + Pubkey::new_unique(), + InterchainGasPaymasterType::OverheadIgp(Pubkey::new_unique()), + )); + + // Set the IGP + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainGasPaymaster(new_igp.clone()) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + // Verify the new IGP is set + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!(token.interchain_gas_paymaster, new_igp); +} + +#[tokio::test] +async fn test_set_interchain_gas_paymaster_errors_if_owner_not_signer() { + let program_id = hyperlane_sealevel_token_collateral_id(); + let spl_token_program_id = spl_token_2022::id(); + + let (mut banks_client, payer) = setup_client().await; + + let (mint, _mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + None, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + let new_igp = Some(( + Pubkey::new_unique(), + InterchainGasPaymasterType::OverheadIgp(Pubkey::new_unique()), + )); + let non_owner = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + + // Try setting the ISM using the mint authority key + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainGasPaymaster(new_igp.clone()) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(non_owner.pubkey(), true), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); + + // Also try using the non_owner as the payer and specifying the correct + // owner account, but the owner isn't a signer: + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainGasPaymaster(new_igp) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), false), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature), + ); +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-memo/Cargo.toml b/rust/sealevel/programs/hyperlane-sealevel-token-memo/Cargo.toml new file mode 100644 index 00000000000..b542953619d --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-memo/Cargo.toml @@ -0,0 +1,46 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "hyperlane-sealevel-token-memo" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] + +[dependencies] +borsh.workspace = true +num-derive.workspace = true +num-traits.workspace = true +solana-program.workspace = true +spl-associated-token-account.workspace = true +spl-noop.workspace = true +spl-token-2022.workspace = true +spl-token.workspace = true +thiserror.workspace = true + +account-utils = { path = "../../libraries/account-utils" } +hyperlane-core = { path = "../../../main/hyperlane-core" } +hyperlane-sealevel-connection-client = { path = "../../libraries/hyperlane-sealevel-connection-client" } +hyperlane-sealevel-mailbox = { path = "../mailbox", features = [ + "no-entrypoint", +] } +hyperlane-sealevel-igp = { path = "../hyperlane-sealevel-igp", features = [ + "no-entrypoint", +] } +hyperlane-sealevel-message-recipient-interface = { path = "../../libraries/message-recipient-interface" } +hyperlane-sealevel-token-lib = { path = "../../libraries/hyperlane-sealevel-token" } +hyperlane-warp-route = { path = "../../../main/applications/hyperlane-warp-route" } +serializable-account-meta = { path = "../../libraries/serializable-account-meta" } + +[dev-dependencies] +solana-program-test.workspace = true +solana-sdk.workspace = true + +hyperlane-test-utils = { path = "../../libraries/test-utils" } +hyperlane-sealevel-test-ism = { path = "../ism/test-ism", features = [ + "no-entrypoint", +] } + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-memo/src/instruction.rs b/rust/sealevel/programs/hyperlane-sealevel-token-memo/src/instruction.rs new file mode 100644 index 00000000000..0bfab1f334f --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-memo/src/instruction.rs @@ -0,0 +1,37 @@ +//! Instructions for the program. + +use hyperlane_sealevel_token_lib::instruction::{init_instruction as lib_init_instruction, Init}; + +use crate::{hyperlane_token_ata_payer_pda_seeds, hyperlane_token_mint_pda_seeds}; + +use solana_program::{ + instruction::{AccountMeta, Instruction as SolanaInstruction}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +/// Gets an instruction to initialize the program. +pub fn init_instruction( + program_id: Pubkey, + payer: Pubkey, + init: Init, +) -> Result { + let mut instruction = lib_init_instruction(program_id, payer, init)?; + + // Add additional account metas: + // 0. `[writable]` The mint / mint authority PDA account. + // 1. `[writable]` The ATA payer PDA account. + + let (mint_key, _mint_bump) = + Pubkey::find_program_address(hyperlane_token_mint_pda_seeds!(), &program_id); + + let (ata_payer_key, _ata_payer_bump) = + Pubkey::find_program_address(hyperlane_token_ata_payer_pda_seeds!(), &program_id); + + instruction.accounts.append(&mut vec![ + AccountMeta::new(mint_key, false), + AccountMeta::new(ata_payer_key, false), + ]); + + Ok(instruction) +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-memo/src/lib.rs b/rust/sealevel/programs/hyperlane-sealevel-token-memo/src/lib.rs new file mode 100644 index 00000000000..b08a06f9f5f --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-memo/src/lib.rs @@ -0,0 +1,14 @@ +//! Hyperlane Token program for synthetic tokens. +#![allow(unexpected_cfgs)] // TODO: `rustc` 1.80.1 clippy issue +#![deny(warnings)] +#![deny(missing_docs)] +#![deny(unsafe_code)] + +pub mod instruction; +pub mod plugin; +pub mod processor; + +pub use spl_associated_token_account; +pub use spl_noop; +pub use spl_token; +pub use spl_token_2022; diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-memo/src/plugin.rs b/rust/sealevel/programs/hyperlane-sealevel-token-memo/src/plugin.rs new file mode 100644 index 00000000000..149062f39bb --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-memo/src/plugin.rs @@ -0,0 +1,375 @@ +//! A plugin for the Hyperlane token program that mints synthetic +//! tokens upon receiving a transfer from a remote chain, and burns +//! synthetic tokens when transferring out to a remote chain. + +use account_utils::{create_pda_account, verify_rent_exempt, SizedData}; +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_sealevel_token_lib::{ + accounts::HyperlaneToken, processor::HyperlaneSealevelTokenPlugin, +}; +use hyperlane_warp_route::TokenMessage; +use serializable_account_meta::SerializableAccountMeta; +#[cfg(not(target_arch = "sbf"))] +use solana_program::program_pack::Pack as _; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + instruction::AccountMeta, + program::{invoke, invoke_signed}, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, + sysvar::Sysvar, +}; +use spl_associated_token_account::{ + get_associated_token_address_with_program_id, + instruction::create_associated_token_account_idempotent, +}; +use spl_token_2022::instruction::{burn_checked, mint_to_checked}; + +/// Seeds relating to the PDA account that acts both as the mint +/// *and* the mint authority. +#[macro_export] +macro_rules! hyperlane_token_mint_pda_seeds { + () => {{ + &[b"hyperlane_token", b"-", b"mint"] + }}; + + ($bump_seed:expr) => {{ + &[b"hyperlane_token", b"-", b"mint", &[$bump_seed]] + }}; +} + +/// Seeds relating to the PDA account that acts as the ATA payer. +#[macro_export] +macro_rules! hyperlane_token_ata_payer_pda_seeds { + () => {{ + &[b"hyperlane_token", b"-", b"ata_payer"] + }}; + + ($bump_seed:expr) => {{ + &[b"hyperlane_token", b"-", b"ata_payer", &[$bump_seed]] + }}; +} + +/// A plugin for the Hyperlane token program that mints synthetic +/// tokens upon receiving a transfer from a remote chain, and burns +/// synthetic tokens when transferring out to a remote chain. +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq, Default)] +pub struct SyntheticPlugin { + /// The mint / mint authority PDA account. + pub mint: Pubkey, + /// The bump seed for the mint / mint authority PDA account. + pub mint_bump: u8, + /// The bump seed for the ATA payer PDA account. + pub ata_payer_bump: u8, +} + +impl SizedData for SyntheticPlugin { + fn size(&self) -> usize { + // mint + 32 + + // mint_bump + std::mem::size_of::() + + // ata_payer_bump + std::mem::size_of::() + } +} + +impl SyntheticPlugin { + /// The size of the mint account. + // Need to hardcode this value because our `spl_token_2022` version doesn't include it. + // It was calculated by calling `ExtensionType::try_calculate_account_len::(vec![ExtensionType::MetadataPointer]).unwrap()` + #[cfg(target_arch = "sbf")] + const MINT_ACCOUNT_SIZE: usize = 234; + /// The size of the mint account. + #[cfg(not(target_arch = "sbf"))] + const MINT_ACCOUNT_SIZE: usize = spl_token_2022::state::Mint::LEN; + + /// Returns Ok(()) if the mint account info is valid. + /// Errors if the key or owner is incorrect. + fn verify_mint_account_info( + program_id: &Pubkey, + token: &HyperlaneToken, + mint_account_info: &AccountInfo, + ) -> Result<(), ProgramError> { + let mint_seeds: &[&[u8]] = hyperlane_token_mint_pda_seeds!(token.plugin_data.mint_bump); + let expected_mint_key = Pubkey::create_program_address(mint_seeds, program_id)?; + if mint_account_info.key != &expected_mint_key { + return Err(ProgramError::InvalidArgument); + } + if *mint_account_info.key != token.plugin_data.mint { + return Err(ProgramError::InvalidArgument); + } + if mint_account_info.owner != &spl_token_2022::id() { + return Err(ProgramError::IncorrectProgramId); + } + + Ok(()) + } + + fn verify_ata_payer_account_info( + program_id: &Pubkey, + token: &HyperlaneToken, + ata_payer_account_info: &AccountInfo, + ) -> Result<(), ProgramError> { + let ata_payer_seeds: &[&[u8]] = + hyperlane_token_ata_payer_pda_seeds!(token.plugin_data.ata_payer_bump); + let expected_ata_payer_account = + Pubkey::create_program_address(ata_payer_seeds, program_id)?; + if ata_payer_account_info.key != &expected_ata_payer_account { + return Err(ProgramError::InvalidArgument); + } + Ok(()) + } +} + +impl HyperlaneSealevelTokenPlugin for SyntheticPlugin { + /// Initializes the plugin. + /// Note this will create a PDA account that will serve as the mint, + /// so the transaction calling this instruction must include a subsequent + /// instruction initializing the mint with the SPL token 2022 program. + /// + /// Accounts: + /// 0. `[writable]` The mint / mint authority PDA account. + /// 1. `[writable]` The ATA payer PDA account. + fn initialize<'a, 'b>( + program_id: &Pubkey, + system_program: &'a AccountInfo<'b>, + _token_account: &'a AccountInfo<'b>, + payer_account: &'a AccountInfo<'b>, + accounts_iter: &mut std::slice::Iter<'a, AccountInfo<'b>>, + ) -> Result { + // Account 0: Mint / mint authority + let mint_account = next_account_info(accounts_iter)?; + let (mint_key, mint_bump) = + Pubkey::find_program_address(hyperlane_token_mint_pda_seeds!(), program_id); + if &mint_key != mint_account.key { + return Err(ProgramError::InvalidArgument); + } + + let rent = Rent::get()?; + + // Create mint / mint authority PDA. + // Grant ownership to the SPL token 2022 program. + create_pda_account( + payer_account, + &rent, + Self::MINT_ACCOUNT_SIZE, + &spl_token_2022::id(), + system_program, + mint_account, + hyperlane_token_mint_pda_seeds!(mint_bump), + )?; + + // Account 1: ATA payer. + let ata_payer_account = next_account_info(accounts_iter)?; + let (ata_payer_key, ata_payer_bump) = + Pubkey::find_program_address(hyperlane_token_ata_payer_pda_seeds!(), program_id); + if &ata_payer_key != ata_payer_account.key { + return Err(ProgramError::InvalidArgument); + } + + // Create the ATA payer. + // This is a separate PDA because the ATA program requires + // the payer to have no data in it. + create_pda_account( + payer_account, + &rent, + 0, + // Grant ownership to the system program so that the ATA program + // can call into the system program with the ATA payer as the + // payer. + &solana_program::system_program::id(), + system_program, + ata_payer_account, + hyperlane_token_ata_payer_pda_seeds!(ata_payer_bump), + )?; + + Ok(Self { + mint: mint_key, + mint_bump, + ata_payer_bump, + }) + } + + /// Transfers tokens into the program so they can be sent to a remote chain. + /// Burns the tokens from the sender's associated token account. + /// + /// Accounts: + /// 0. `[executable]` The spl_token_2022 program. + /// 1. `[writeable]` The mint / mint authority PDA account. + /// 2. `[writeable]` The token sender's associated token account, from which tokens will be burned. + fn transfer_in<'a, 'b>( + program_id: &Pubkey, + token: &HyperlaneToken, + sender_wallet: &'a AccountInfo<'b>, + accounts_iter: &mut std::slice::Iter<'a, AccountInfo<'b>>, + amount: u64, + ) -> Result<(), ProgramError> { + // 0. SPL token 2022 program + let spl_token_2022 = next_account_info(accounts_iter)?; + if spl_token_2022.key != &spl_token_2022::id() || !spl_token_2022.executable { + return Err(ProgramError::InvalidArgument); + } + + // 1. The mint / mint authority. + let mint_account = next_account_info(accounts_iter)?; + Self::verify_mint_account_info(program_id, token, mint_account)?; + + // 2. The sender's associated token account. + let sender_ata = next_account_info(accounts_iter)?; + let expected_sender_associated_token_account = get_associated_token_address_with_program_id( + sender_wallet.key, + mint_account.key, + &spl_token_2022::id(), + ); + if sender_ata.key != &expected_sender_associated_token_account { + return Err(ProgramError::InvalidArgument); + } + + let burn_ixn = burn_checked( + &spl_token_2022::id(), + sender_ata.key, + mint_account.key, + sender_wallet.key, + &[sender_wallet.key], + amount, + token.decimals, + )?; + // Sender wallet is expected to have signed this transaction + invoke( + &burn_ixn, + &[ + sender_ata.clone(), + mint_account.clone(), + sender_wallet.clone(), + ], + )?; + + Ok(()) + } + + /// Transfers tokens out to a recipient's associated token account as a + /// result of a transfer to this chain from a remote chain. + /// + /// Accounts: + /// 0. `[executable]` SPL token 2022 program + /// 1. `[executable]` SPL associated token account + /// 2. `[writeable]` Mint account + /// 3. `[writeable]` Recipient associated token account + /// 4. `[writeable]` ATA payer PDA account. + fn transfer_out<'a, 'b>( + program_id: &Pubkey, + token: &HyperlaneToken, + system_program: &'a AccountInfo<'b>, + recipient_wallet: &'a AccountInfo<'b>, + accounts_iter: &mut std::slice::Iter<'a, AccountInfo<'b>>, + amount: u64, + ) -> Result<(), ProgramError> { + // Account 0: SPL token 2022 program + let spl_token_2022 = next_account_info(accounts_iter)?; + if spl_token_2022.key != &spl_token_2022::id() || !spl_token_2022.executable { + return Err(ProgramError::InvalidArgument); + } + // Account 1: SPL associated token account + let spl_ata = next_account_info(accounts_iter)?; + if spl_ata.key != &spl_associated_token_account::id() || !spl_ata.executable { + return Err(ProgramError::InvalidArgument); + } + + // Account 2: Mint account + let mint_account = next_account_info(accounts_iter)?; + Self::verify_mint_account_info(program_id, token, mint_account)?; + + // Account 3: Recipient associated token account + let recipient_ata = next_account_info(accounts_iter)?; + let expected_recipient_associated_token_account = + get_associated_token_address_with_program_id( + recipient_wallet.key, + mint_account.key, + &spl_token_2022::id(), + ); + if recipient_ata.key != &expected_recipient_associated_token_account { + return Err(ProgramError::InvalidArgument); + } + + // Account 4: ATA payer PDA account + let ata_payer_account = next_account_info(accounts_iter)?; + Self::verify_ata_payer_account_info(program_id, token, ata_payer_account)?; + + // Create and init (this does both) associated token account if necessary. + invoke_signed( + &create_associated_token_account_idempotent( + ata_payer_account.key, + recipient_wallet.key, + mint_account.key, + &spl_token_2022::id(), + ), + &[ + ata_payer_account.clone(), + recipient_ata.clone(), + recipient_wallet.clone(), + mint_account.clone(), + system_program.clone(), + spl_token_2022.clone(), + ], + &[hyperlane_token_ata_payer_pda_seeds!( + token.plugin_data.ata_payer_bump + )], + )?; + + // After potentially paying for the ATA creation, we need to make sure + // the ATA payer still meets the rent-exemption requirements. + verify_rent_exempt(recipient_ata, &Rent::get()?)?; + + let mint_ixn = mint_to_checked( + &spl_token_2022::id(), + mint_account.key, + recipient_ata.key, + mint_account.key, + &[], + amount, + token.decimals, + )?; + invoke_signed( + &mint_ixn, + &[ + mint_account.clone(), + recipient_ata.clone(), + mint_account.clone(), + ], + &[hyperlane_token_mint_pda_seeds!(token.plugin_data.mint_bump)], + )?; + + Ok(()) + } + + fn transfer_out_account_metas( + program_id: &Pubkey, + token: &HyperlaneToken, + token_message: &TokenMessage, + ) -> Result<(Vec, bool), ProgramError> { + let ata_payer_account_key = Pubkey::create_program_address( + hyperlane_token_ata_payer_pda_seeds!(token.plugin_data.ata_payer_bump), + program_id, + )?; + + let recipient_associated_token_account = get_associated_token_address_with_program_id( + &Pubkey::new_from_array(token_message.recipient().into()), + &token.plugin_data.mint, + &spl_token_2022::id(), + ); + + Ok(( + vec![ + AccountMeta::new_readonly(spl_token_2022::id(), false).into(), + AccountMeta::new_readonly(spl_associated_token_account::id(), false).into(), + AccountMeta::new(token.plugin_data.mint, false).into(), + AccountMeta::new(recipient_associated_token_account, false).into(), + AccountMeta::new(ata_payer_account_key, false).into(), + ], + // The recipient does not need to be writeable + false, + )) + } +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-memo/src/processor.rs b/rust/sealevel/programs/hyperlane-sealevel-token-memo/src/processor.rs new file mode 100644 index 00000000000..b98a51d4bfb --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-memo/src/processor.rs @@ -0,0 +1,290 @@ +//! Program processor. + +use account_utils::DiscriminatorDecode; +use hyperlane_sealevel_connection_client::{ + gas_router::GasRouterConfig, router::RemoteRouterConfig, +}; +use hyperlane_sealevel_igp::accounts::InterchainGasPaymasterType; +use hyperlane_sealevel_message_recipient_interface::{ + HandleInstruction, MessageRecipientInstruction, +}; +use hyperlane_sealevel_token_lib::{ + instruction::{ + DymInstruction, Init, Instruction as TokenIxn, TransferRemote, TransferRemoteMemo, + }, + processor::HyperlaneSealevelToken, +}; +use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, msg, pubkey::Pubkey}; + +use crate::plugin::SyntheticPlugin; + +#[cfg(not(feature = "no-entrypoint"))] +solana_program::entrypoint!(process_instruction); + +/// Processes an instruction. +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + // First, check if the instruction has a discriminant relating to + // the message recipient interface. + if let Ok(message_recipient_instruction) = MessageRecipientInstruction::decode(instruction_data) + { + return match message_recipient_instruction { + MessageRecipientInstruction::InterchainSecurityModule => { + interchain_security_module(program_id, accounts) + } + MessageRecipientInstruction::InterchainSecurityModuleAccountMetas => { + interchain_security_module_account_metas(program_id) + } + MessageRecipientInstruction::Handle(handle) => transfer_from_remote( + program_id, + accounts, + HandleInstruction { + origin: handle.origin, + sender: handle.sender, + message: handle.message, + }, + ), + MessageRecipientInstruction::HandleAccountMetas(handle) => { + transfer_from_remote_account_metas( + program_id, + accounts, + HandleInstruction { + origin: handle.origin, + sender: handle.sender, + message: handle.message, + }, + ) + } + }; + } + if let Ok(instr) = DymInstruction::decode(instruction_data) { + return match instr { + DymInstruction::TransferRemoteMemo(xfer) => { + transfer_remote_memo(program_id, accounts, xfer) + } + } + .map_err(|err| { + msg!("{}", err); + err + }); + } + + // Otherwise, try decoding a "normal" token instruction + match TokenIxn::decode(instruction_data)? { + TokenIxn::Init(init) => initialize(program_id, accounts, init), + TokenIxn::TransferRemote(xfer) => transfer_remote(program_id, accounts, xfer), + TokenIxn::EnrollRemoteRouter(config) => enroll_remote_router(program_id, accounts, config), + TokenIxn::EnrollRemoteRouters(configs) => { + enroll_remote_routers(program_id, accounts, configs) + } + TokenIxn::SetDestinationGasConfigs(configs) => { + set_destination_gas_configs(program_id, accounts, configs) + } + TokenIxn::SetInterchainSecurityModule(new_ism) => { + set_interchain_security_module(program_id, accounts, new_ism) + } + TokenIxn::SetInterchainGasPaymaster(new_igp) => { + set_interchain_gas_paymaster(program_id, accounts, new_igp) + } + TokenIxn::TransferOwnership(new_owner) => { + transfer_ownership(program_id, accounts, new_owner) + } + } + .map_err(|err| { + msg!("{}", err); + err + }) +} + +fn transfer_remote_memo( + program_id: &Pubkey, + accounts: &[AccountInfo], + transfer: TransferRemoteMemo, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_remote_memo( + program_id, + accounts, + transfer.base, + transfer.memo, + ) +} + +/// Initializes the program. +/// +/// Accounts: +/// 0. `[executable]` The system program. +/// 1. `[writable]` The token PDA account. +/// 2. `[writable]` The dispatch authority PDA account. +/// 3. `[signer]` The payer. +/// 4. `[writable]` The mint / mint authority PDA account. +/// 5. `[writable]` The ATA payer PDA account. +fn initialize(program_id: &Pubkey, accounts: &[AccountInfo], init: Init) -> ProgramResult { + HyperlaneSealevelToken::::initialize(program_id, accounts, init) +} + +/// Transfers tokens to a remote. +/// Burns the tokens from the sender's associated token account and +/// then dispatches a message to the remote recipient. +/// +/// Accounts: +/// 0. `[executable]` The system program. +/// 1. `[executable]` The spl_noop program. +/// 2. `[]` The token PDA account. +/// 3. `[executable]` The mailbox program. +/// 4. `[writeable]` The mailbox outbox account. +/// 5. `[]` Message dispatch authority. +/// 6. `[signer]` The token sender and mailbox payer. +/// 7. `[signer]` Unique message / gas payment account. +/// 8. `[writeable]` Message storage PDA. +/// ---- If using an IGP ---- +/// 9. `[executable]` The IGP program. +/// 10. `[writeable]` The IGP program data. +/// 11. `[writeable]` Gas payment PDA. +/// 12. `[]` OPTIONAL - The Overhead IGP program, if the configured IGP is an Overhead IGP. +/// 13. `[writeable]` The IGP account. +/// ---- End if ---- +/// 14. `[executable]` The spl_token_2022 program. +/// 15. `[writeable]` The mint / mint authority PDA account. +/// 16. `[writeable]` The token sender's associated token account, from which tokens will be burned. +fn transfer_remote( + program_id: &Pubkey, + accounts: &[AccountInfo], + transfer: TransferRemote, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_remote(program_id, accounts, transfer) +} + +// Accounts: +// 0. `[signer]` Mailbox process authority specific to this program. +// 1. `[executable]` system_program +// 2. `[]` hyperlane_token storage +// 3. `[]` recipient wallet address +// 4. `[executable]` SPL token 2022 program +// 5. `[executable]` SPL associated token account +// 6. `[writeable]` Mint account +// 7. `[writeable]` Recipient associated token account +// 8. `[writeable]` ATA payer PDA account. +fn transfer_from_remote( + program_id: &Pubkey, + accounts: &[AccountInfo], + transfer: HandleInstruction, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_from_remote(program_id, accounts, transfer) +} + +/// Gets the account metas required for the `HandleInstruction` instruction. +fn transfer_from_remote_account_metas( + program_id: &Pubkey, + accounts: &[AccountInfo], + transfer: HandleInstruction, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_from_remote_account_metas( + program_id, accounts, transfer, + ) +} + +/// Enrolls a remote router. +/// +/// Accounts: +/// 0. `[executable]` The system program. +/// 1. `[writeable]` The token PDA account. +/// 2. `[signer]` The owner. +fn enroll_remote_router( + program_id: &Pubkey, + accounts: &[AccountInfo], + config: RemoteRouterConfig, +) -> ProgramResult { + HyperlaneSealevelToken::::enroll_remote_router(program_id, accounts, config) +} + +/// Enrolls remote routers. +/// +/// Accounts: +/// 0. `[executable]` The system program. +/// 1. `[writeable]` The token PDA account. +/// 2. `[signer]` The owner. +fn enroll_remote_routers( + program_id: &Pubkey, + accounts: &[AccountInfo], + configs: Vec, +) -> ProgramResult { + HyperlaneSealevelToken::::enroll_remote_routers(program_id, accounts, configs) +} + +/// Sets the destination gas configs. +/// +/// Accounts: +/// 0. `[executable]` The system program. +/// 1. `[writeable]` The token PDA account. +/// 2. `[signer]` The owner. +fn set_destination_gas_configs( + program_id: &Pubkey, + accounts: &[AccountInfo], + configs: Vec, +) -> ProgramResult { + HyperlaneSealevelToken::::set_destination_gas_configs( + program_id, accounts, configs, + ) +} + +/// Transfers ownership. +/// +/// Accounts: +/// 0. `[writeable]` The token PDA account. +/// 1. `[signer]` The current owner. +fn transfer_ownership( + program_id: &Pubkey, + accounts: &[AccountInfo], + new_owner: Option, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_ownership(program_id, accounts, new_owner) +} + +/// Gets the interchain security module, returning it as a serialized Option. +/// +/// Accounts: +/// 0. `[]` The token PDA account. +fn interchain_security_module(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + HyperlaneSealevelToken::::interchain_security_module(program_id, accounts) +} + +/// Gets the account metas for getting the interchain security module. +/// +/// Accounts: +/// None +fn interchain_security_module_account_metas(program_id: &Pubkey) -> ProgramResult { + HyperlaneSealevelToken::::interchain_security_module_account_metas(program_id) +} + +/// Lets the owner set the interchain security module. +/// +/// Accounts: +/// 0. `[writeable]` The token PDA account. +/// 1. `[signer]` The access control owner. +fn set_interchain_security_module( + program_id: &Pubkey, + accounts: &[AccountInfo], + new_ism: Option, +) -> ProgramResult { + HyperlaneSealevelToken::::set_interchain_security_module( + program_id, accounts, new_ism, + ) +} + +/// Lets the owner set the interchain gas paymaster. +/// +/// Accounts: +/// 0. `[writeable]` The token PDA account. +/// 1. `[signer]` The access control owner. +fn set_interchain_gas_paymaster( + program_id: &Pubkey, + accounts: &[AccountInfo], + new_igp: Option<(Pubkey, InterchainGasPaymasterType)>, +) -> ProgramResult { + HyperlaneSealevelToken::::set_interchain_gas_paymaster( + program_id, accounts, new_igp, + ) +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-memo/tests/functional.rs b/rust/sealevel/programs/hyperlane-sealevel-token-memo/tests/functional.rs new file mode 100644 index 00000000000..b4af2604021 --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-memo/tests/functional.rs @@ -0,0 +1,1365 @@ +//! Contains functional tests for things that cannot be done +//! strictly in unit tests. This includes CPIs, like creating +//! new PDA accounts. + +use account_utils::DiscriminatorEncode; +use hyperlane_core::{Encode, HyperlaneMessage, H256, U256}; +use hyperlane_sealevel_connection_client::{ + gas_router::GasRouterConfig, router::RemoteRouterConfig, +}; +use hyperlane_sealevel_igp::{ + accounts::{GasPaymentAccount, GasPaymentData, InterchainGasPaymasterType}, + igp_gas_payment_pda_seeds, +}; +use hyperlane_sealevel_mailbox::{ + accounts::{DispatchedMessage, DispatchedMessageAccount}, + mailbox_dispatched_message_pda_seeds, mailbox_message_dispatch_authority_pda_seeds, + mailbox_process_authority_pda_seeds, + protocol_fee::ProtocolFee, +}; +use hyperlane_sealevel_message_recipient_interface::{ + HandleInstruction, MessageRecipientInstruction, +}; +use hyperlane_sealevel_token_lib::{ + accounts::{convert_decimals, HyperlaneToken, HyperlaneTokenAccount}, + hyperlane_token_pda_seeds, + instruction::{ + DymInstruction, Init, Instruction as HyperlaneTokenInstruction, TransferRemote, + TransferRemoteMemo, + }, +}; +use hyperlane_sealevel_token_memo::processor::process_instruction; +use hyperlane_sealevel_token_memo::{ + hyperlane_token_ata_payer_pda_seeds, hyperlane_token_mint_pda_seeds, plugin::SyntheticPlugin, +}; +use hyperlane_test_utils::{ + assert_token_balance, assert_transaction_error, igp_program_id, initialize_igp_accounts, + initialize_mailbox, mailbox_id, new_funded_keypair, process, transfer_lamports, IgpAccounts, + MailboxAccounts, +}; +use hyperlane_warp_route::TokenMessage; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey, + pubkey::Pubkey, +}; +use solana_program_test::*; +use solana_sdk::{ + instruction::InstructionError, + signature::Signer, + signer::keypair::Keypair, + transaction::{Transaction, TransactionError}, +}; +use spl_token_2022::instruction::initialize_mint2; +use std::collections::HashMap; + +/// There are 1e9 lamports in one SOL. +const ONE_SOL_IN_LAMPORTS: u64 = 1000000000; +const LOCAL_DOMAIN: u32 = 1234; +const LOCAL_DECIMALS: u8 = 8; +const LOCAL_DECIMALS_U32: u32 = LOCAL_DECIMALS as u32; +const REMOTE_DOMAIN: u32 = 4321; +const REMOTE_DECIMALS: u8 = 18; +const REMOTE_GAS_AMOUNT: u64 = 200000; + +fn hyperlane_sealevel_token_id() -> Pubkey { + pubkey!("3MzUPjP5LEkiHH82nEAe28Xtz9ztuMqWc8UmuKxrpVQH") +} + +async fn setup_client() -> (BanksClient, Keypair) { + let program_id = hyperlane_sealevel_token_id(); + let mut program_test = ProgramTest::new( + "hyperlane_sealevel_token", + program_id, + processor!(process_instruction), + ); + + program_test.add_program( + "spl_token_2022", + spl_token_2022::id(), + processor!(spl_token_2022::processor::Processor::process), + ); + + program_test.add_program( + "spl_associated_token_account", + spl_associated_token_account::id(), + processor!(spl_associated_token_account::processor::process_instruction), + ); + + program_test.add_program("spl_noop", spl_noop::id(), processor!(spl_noop::noop)); + + let mailbox_program_id = mailbox_id(); + program_test.add_program( + "hyperlane_sealevel_mailbox", + mailbox_program_id, + processor!(hyperlane_sealevel_mailbox::processor::process_instruction), + ); + + program_test.add_program( + "hyperlane_sealevel_igp", + igp_program_id(), + processor!(hyperlane_sealevel_igp::processor::process_instruction), + ); + + // This serves as the default ISM on the Mailbox + program_test.add_program( + "hyperlane_sealevel_test_ism", + hyperlane_sealevel_test_ism::id(), + processor!(hyperlane_sealevel_test_ism::program::process_instruction), + ); + + let (banks_client, payer, _recent_blockhash) = program_test.start().await; + + (banks_client, payer) +} + +struct HyperlaneTokenAccounts { + token: Pubkey, + token_bump: u8, + mailbox_process_authority: Pubkey, + dispatch_authority: Pubkey, + dispatch_authority_bump: u8, + mint: Pubkey, + mint_bump: u8, + ata_payer: Pubkey, + ata_payer_bump: u8, +} + +async fn initialize_hyperlane_token( + program_id: &Pubkey, + banks_client: &mut BanksClient, + payer: &Keypair, + igp_accounts: Option<&IgpAccounts>, +) -> Result { + let (mailbox_process_authority_key, _mailbox_process_authority_bump) = + Pubkey::find_program_address( + mailbox_process_authority_pda_seeds!(program_id), + &mailbox_id(), + ); + + let (token_account_key, token_account_bump_seed) = + Pubkey::find_program_address(hyperlane_token_pda_seeds!(), program_id); + + let (dispatch_authority_key, dispatch_authority_seed) = + Pubkey::find_program_address(mailbox_message_dispatch_authority_pda_seeds!(), program_id); + + let (mint_account_key, mint_account_bump_seed) = + Pubkey::find_program_address(hyperlane_token_mint_pda_seeds!(), program_id); + + let (ata_payer_account_key, ata_payer_account_bump_seed) = + Pubkey::find_program_address(hyperlane_token_ata_payer_pda_seeds!(), program_id); + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[ + Instruction::new_with_bytes( + *program_id, + &HyperlaneTokenInstruction::Init(Init { + mailbox: mailbox_id(), + interchain_security_module: None, + interchain_gas_paymaster: igp_accounts.map(|igp_accounts| { + ( + igp_accounts.program, + InterchainGasPaymasterType::OverheadIgp(igp_accounts.overhead_igp), + ) + }), + decimals: LOCAL_DECIMALS, + remote_decimals: REMOTE_DECIMALS, + }) + .encode() + .unwrap(), + vec![ + // 0. `[executable]` The system program. + // 1. `[writable]` The token PDA account. + // 2. `[writable]` The dispatch authority PDA account. + // 3. `[signer]` The payer. + // 4. `[writable]` The mint / mint authority PDA account. + // 5. `[writable]` The ATA payer PDA account. + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(token_account_key, false), + AccountMeta::new(dispatch_authority_key, false), + AccountMeta::new_readonly(payer.pubkey(), true), + AccountMeta::new(mint_account_key, false), + AccountMeta::new(ata_payer_account_key, false), + ], + ), + initialize_mint2( + &spl_token_2022::id(), + &mint_account_key, + &mint_account_key, + None, + LOCAL_DECIMALS, + ) + .unwrap(), + ], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await?; + + // Set destination gas configs + set_destination_gas_config( + banks_client, + program_id, + payer, + &token_account_key, + REMOTE_DOMAIN, + REMOTE_GAS_AMOUNT, + ) + .await?; + + Ok(HyperlaneTokenAccounts { + token: token_account_key, + token_bump: token_account_bump_seed, + mailbox_process_authority: mailbox_process_authority_key, + dispatch_authority: dispatch_authority_key, + dispatch_authority_bump: dispatch_authority_seed, + mint: mint_account_key, + mint_bump: mint_account_bump_seed, + ata_payer: ata_payer_account_key, + ata_payer_bump: ata_payer_account_bump_seed, + }) +} + +async fn enroll_remote_router( + banks_client: &mut BanksClient, + program_id: &Pubkey, + payer: &Keypair, + token_account: &Pubkey, + domain: u32, + router: H256, +) -> Result<(), BanksClientError> { + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Enroll the remote router + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + *program_id, + &HyperlaneTokenInstruction::EnrollRemoteRouter(RemoteRouterConfig { + domain, + router: Some(router), + }) + .encode() + .unwrap(), + vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(*token_account, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await?; + + Ok(()) +} + +async fn set_destination_gas_config( + banks_client: &mut BanksClient, + program_id: &Pubkey, + payer: &Keypair, + token_account: &Pubkey, + domain: u32, + gas: u64, +) -> Result<(), BanksClientError> { + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Enroll the remote router + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + *program_id, + &HyperlaneTokenInstruction::SetDestinationGasConfigs(vec![GasRouterConfig { + domain, + gas: Some(gas), + }]) + .encode() + .unwrap(), + vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(*token_account, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await?; + + Ok(()) +} + +#[tokio::test] +async fn test_initialize() { + let program_id = hyperlane_sealevel_token_id(); + let mailbox_program_id = mailbox_id(); + + let (mut banks_client, payer) = setup_client().await; + + let mailbox_accounts = initialize_mailbox( + &mut banks_client, + &mailbox_program_id, + &payer, + LOCAL_DOMAIN, + ONE_SOL_IN_LAMPORTS, + ProtocolFee::default(), + ) + .await + .unwrap(); + + let igp_accounts = + initialize_igp_accounts(&mut banks_client, &igp_program_id(), &payer, REMOTE_DOMAIN) + .await + .unwrap(); + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, Some(&igp_accounts)) + .await + .unwrap(); + + // Get the token account. + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!( + token, + Box::new(HyperlaneToken { + bump: hyperlane_token_accounts.token_bump, + mailbox: mailbox_accounts.program, + mailbox_process_authority: hyperlane_token_accounts.mailbox_process_authority, + dispatch_authority_bump: hyperlane_token_accounts.dispatch_authority_bump, + decimals: LOCAL_DECIMALS, + remote_decimals: REMOTE_DECIMALS, + owner: Some(payer.pubkey()), + interchain_security_module: None, + interchain_gas_paymaster: Some(( + igp_accounts.program, + InterchainGasPaymasterType::OverheadIgp(igp_accounts.overhead_igp), + )), + destination_gas: HashMap::from([(REMOTE_DOMAIN, REMOTE_GAS_AMOUNT)]), + remote_routers: HashMap::new(), + plugin_data: SyntheticPlugin { + mint: hyperlane_token_accounts.mint, + mint_bump: hyperlane_token_accounts.mint_bump, + ata_payer_bump: hyperlane_token_accounts.ata_payer_bump, + }, + }), + ); + + // Verify the mint account was created. + let mint_account = banks_client + .get_account(hyperlane_token_accounts.mint) + .await + .unwrap() + .unwrap(); + assert_eq!(mint_account.owner, spl_token_2022::id()); + assert!(!mint_account.data.is_empty()); + + // Verify the ATA payer account was created. + let ata_payer_account = banks_client + .get_account(hyperlane_token_accounts.ata_payer) + .await + .unwrap() + .unwrap(); + assert!(ata_payer_account.lamports > 0); +} + +#[tokio::test] +async fn test_initialize_errors_if_called_twice() { + let program_id = hyperlane_sealevel_token_id(); + + let (mut banks_client, payer) = setup_client().await; + + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + let new_payer = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + + // To ensure a different signature is used, we'll use a different payer + let init_result = + initialize_hyperlane_token(&program_id, &mut banks_client, &new_payer, None).await; + + assert_transaction_error( + init_result, + TransactionError::InstructionError(0, InstructionError::AccountAlreadyInitialized), + ); +} + +async fn transfer_from_remote( + remote_transfer_amount: U256, + sender_override: Option, + origin_override: Option, + recipient_wallet: Option, +) -> Result< + ( + BanksClient, + Keypair, + MailboxAccounts, + IgpAccounts, + HyperlaneTokenAccounts, + Pubkey, + ), + BanksClientError, +> { + let program_id = hyperlane_sealevel_token_id(); + let mailbox_program_id = mailbox_id(); + + let (mut banks_client, payer) = setup_client().await; + + let mailbox_accounts = initialize_mailbox( + &mut banks_client, + &mailbox_program_id, + &payer, + LOCAL_DOMAIN, + ONE_SOL_IN_LAMPORTS, + ProtocolFee::default(), + ) + .await + .unwrap(); + + let igp_accounts = + initialize_igp_accounts(&mut banks_client, &igp_program_id(), &payer, REMOTE_DOMAIN) + .await + .unwrap(); + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, Some(&igp_accounts)) + .await + .unwrap(); + // ATA payer must have a balance to create new ATAs + transfer_lamports( + &mut banks_client, + &payer, + &hyperlane_token_accounts.ata_payer, + ONE_SOL_IN_LAMPORTS, + ) + .await; + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + let recipient_pubkey = recipient_wallet.unwrap_or_else(Pubkey::new_unique); + let recipient: H256 = recipient_pubkey.to_bytes().into(); + + let recipient_associated_token_account = + spl_associated_token_account::get_associated_token_address_with_program_id( + &recipient_pubkey, + &hyperlane_token_accounts.mint, + &spl_token_2022::id(), + ); + + let message = HyperlaneMessage { + version: 3, + nonce: 0, + origin: origin_override.unwrap_or(REMOTE_DOMAIN), + // Default to the remote router as the sender + sender: sender_override.unwrap_or(remote_router), + destination: LOCAL_DOMAIN, + recipient: program_id.to_bytes().into(), + body: TokenMessage::new(recipient, remote_transfer_amount, vec![]).to_vec(), + }; + + process( + &mut banks_client, + &payer, + &mailbox_accounts, + vec![], + &message, + ) + .await?; + + Ok(( + banks_client, + payer, + mailbox_accounts, + igp_accounts, + hyperlane_token_accounts, + recipient_associated_token_account, + )) +} + +// Tests when the SPL token is the 2022 version +#[tokio::test] +async fn test_transfer_from_remote() { + let local_transfer_amount = 69 * 10u64.pow(LOCAL_DECIMALS_U32); + let remote_transfer_amount = convert_decimals( + local_transfer_amount.into(), + LOCAL_DECIMALS, + REMOTE_DECIMALS, + ) + .unwrap(); + + let ( + mut banks_client, + _payer, + _mailbox_accounts, + _igp_accounts, + _hyperlane_token_accounts, + recipient_associated_token_account, + ) = transfer_from_remote(remote_transfer_amount, None, None, None) + .await + .unwrap(); + + // Check that the recipient's ATA got the tokens! + assert_token_balance( + &mut banks_client, + &recipient_associated_token_account, + local_transfer_amount, + ) + .await; +} + +#[tokio::test] +async fn test_transfer_from_remote_errors_if_sender_not_router() { + let local_transfer_amount = 69 * 10u64.pow(LOCAL_DECIMALS_U32); + let remote_transfer_amount = convert_decimals( + local_transfer_amount.into(), + LOCAL_DECIMALS, + REMOTE_DECIMALS, + ) + .unwrap(); + + // Same remote domain origin, but wrong sender. + let result = + transfer_from_remote(remote_transfer_amount, Some(H256::random()), None, None).await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidInstructionData), + ); + + // Wrong remote domain origin, but correct sender. + let result = + transfer_from_remote(remote_transfer_amount, None, Some(REMOTE_DOMAIN + 1), None).await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidInstructionData), + ); +} + +#[tokio::test] +async fn test_transfer_from_remote_errors_if_process_authority_not_signer() { + let program_id = hyperlane_sealevel_token_id(); + let mailbox_program_id = mailbox_id(); + + let (mut banks_client, payer) = setup_client().await; + + let _mailbox_accounts = initialize_mailbox( + &mut banks_client, + &mailbox_program_id, + &payer, + LOCAL_DOMAIN, + ONE_SOL_IN_LAMPORTS, + ProtocolFee::default(), + ) + .await + .unwrap(); + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + let recipient_pubkey = Pubkey::new_unique(); + let recipient: H256 = recipient_pubkey.to_bytes().into(); + + let recipient_associated_token_account = + spl_associated_token_account::get_associated_token_address_with_program_id( + &recipient_pubkey, + &hyperlane_token_accounts.mint, + &spl_token_2022::id(), + ); + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Try calling directly into the message handler, skipping the mailbox. + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &MessageRecipientInstruction::Handle(HandleInstruction { + origin: REMOTE_DOMAIN, + sender: remote_router, + message: TokenMessage::new(recipient, 12345u64.into(), vec![]).to_vec(), + }) + .encode() + .unwrap(), + vec![ + // Recipient.handle accounts + // 0. `[signer]` Mailbox process authority specific to this program. + // 1. `[executable]` system_program + // 2. `[]` hyperlane_token storage + // 3. `[]` recipient wallet address + // 4. `[executable]` SPL token 2022 program + // 5. `[executable]` SPL associated token account + // 6. `[writeable]` Mint account + // 7. `[writeable]` Recipient associated token account + // 8. `[writeable]` ATA payer PDA account. + AccountMeta::new_readonly( + hyperlane_token_accounts.mailbox_process_authority, + false, + ), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new_readonly(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(recipient_pubkey, false), + AccountMeta::new_readonly(spl_token_2022::id(), false), + AccountMeta::new_readonly(spl_associated_token_account::id(), false), + AccountMeta::new(hyperlane_token_accounts.mint, false), + AccountMeta::new(recipient_associated_token_account, false), + AccountMeta::new(hyperlane_token_accounts.ata_payer, false), + ], + )], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature), + ); +} + +#[tokio::test] +async fn test_transfer_remote_memo() { + let program_id = hyperlane_sealevel_token_id(); + let mailbox_program_id = mailbox_id(); + + let token_sender = Keypair::new(); + let token_sender_pubkey = token_sender.pubkey(); + + // Mint 100 tokens to the token sender's ATA. + // We do this by just faking a transfer from remote. + let sender_initial_balance = 100 * 10u64.pow(LOCAL_DECIMALS_U32); + let ( + mut banks_client, + payer, + mailbox_accounts, + igp_accounts, + hyperlane_token_accounts, + token_sender_ata, + ) = transfer_from_remote( + // The amount of remote tokens is expected + convert_decimals( + sender_initial_balance.into(), + LOCAL_DECIMALS, + REMOTE_DECIMALS, + ) + .unwrap(), + None, + None, + Some(token_sender_pubkey), + ) + .await + .unwrap(); + + // Give the token_sender a SOL balance to pay tx fees. + transfer_lamports( + &mut banks_client, + &payer, + &token_sender_pubkey, + ONE_SOL_IN_LAMPORTS, + ) + .await; + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + // Call transfer_remote + let unique_message_account_keypair = Keypair::new(); + let (dispatched_message_key, _dispatched_message_bump) = Pubkey::find_program_address( + mailbox_dispatched_message_pda_seeds!(&unique_message_account_keypair.pubkey()), + &mailbox_program_id, + ); + let (gas_payment_pda_key, _gas_payment_pda_bump) = Pubkey::find_program_address( + igp_gas_payment_pda_seeds!(&unique_message_account_keypair.pubkey()), + &igp_program_id(), + ); + + let remote_token_recipient = H256::random(); + // Transfer 69 tokens. + let transfer_amount = 69 * 10u64.pow(LOCAL_DECIMALS_U32); + let remote_transfer_amount = + convert_decimals(transfer_amount.into(), LOCAL_DECIMALS, REMOTE_DECIMALS).unwrap(); + + let test_memo = vec![1, 2, 3]; + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &DymInstruction::TransferRemoteMemo(TransferRemoteMemo { + base: TransferRemote { + destination_domain: REMOTE_DOMAIN, + recipient: remote_token_recipient, + amount_or_id: transfer_amount.into(), + }, + memo: test_memo.clone(), + }) + .encode() + .unwrap(), + // 0. `[executable]` The system program. + // 1. `[executable]` The spl_noop program. + // 2. `[]` The token PDA account. + // 3. `[executable]` The mailbox program. + // 4. `[writeable]` The mailbox outbox account. + // 5. `[]` Message dispatch authority. + // 6. `[signer]` The token sender and mailbox payer. + // 7. `[signer]` Unique message account. + // 8. `[writeable]` Message storage PDA. + // ---- If using an IGP ---- + // 9. `[executable]` The IGP program. + // 10. `[writeable]` The IGP program data. + // 11. `[writeable]` Gas payment PDA. + // 12. `[]` OPTIONAL - The Overhead IGP program, if the configured IGP is an Overhead IGP. + // 13. `[writeable]` The IGP account. + // ---- End if ---- + // 14. `[executable]` The spl_token_2022 program. + // 15. `[writeable]` The mint / mint authority PDA account. + // 16. `[writeable]` The token sender's associated token account, from which tokens will be burned. + vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new_readonly(spl_noop::id(), false), + AccountMeta::new_readonly(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(mailbox_accounts.program, false), + AccountMeta::new(mailbox_accounts.outbox, false), + AccountMeta::new_readonly(hyperlane_token_accounts.dispatch_authority, false), + AccountMeta::new_readonly(token_sender_pubkey, true), + AccountMeta::new_readonly(unique_message_account_keypair.pubkey(), true), + AccountMeta::new(dispatched_message_key, false), + AccountMeta::new_readonly(igp_accounts.program, false), + AccountMeta::new(igp_accounts.program_data, false), + AccountMeta::new(gas_payment_pda_key, false), + AccountMeta::new_readonly(igp_accounts.overhead_igp, false), + AccountMeta::new(igp_accounts.igp, false), + AccountMeta::new_readonly(spl_token_2022::id(), false), + AccountMeta::new(hyperlane_token_accounts.mint, false), + AccountMeta::new(token_sender_ata, false), + ], + )], + Some(&token_sender_pubkey), + &[&token_sender, &unique_message_account_keypair], + recent_blockhash, + ); + let tx_signature = transaction.signatures[0]; + banks_client.process_transaction(transaction).await.unwrap(); + + // Verify the token sender's ATA balance went down + assert_token_balance( + &mut banks_client, + &token_sender_ata, + sender_initial_balance - transfer_amount, + ) + .await; + + // And let's take a look at the dispatched message account data to verify the message looks right. + let dispatched_message_account_data = banks_client + .get_account(dispatched_message_key) + .await + .unwrap() + .unwrap() + .data; + let dispatched_message = + DispatchedMessageAccount::fetch(&mut &dispatched_message_account_data[..]) + .unwrap() + .into_inner(); + + let transfer_remote_tx_status = banks_client + .get_transaction_status(tx_signature) + .await + .unwrap() + .unwrap(); + + let message = HyperlaneMessage { + version: 3, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: program_id.to_bytes().into(), + destination: REMOTE_DOMAIN, + recipient: remote_router, + // Expect the remote_transfer_amount to be in the message. + body: TokenMessage::new(remote_token_recipient, remote_transfer_amount, test_memo).to_vec(), + }; + + assert_eq!( + dispatched_message, + Box::new(DispatchedMessage::new( + message.nonce, + transfer_remote_tx_status.slot, + unique_message_account_keypair.pubkey(), + message.to_vec(), + )), + ); + + // And let's also look at the gas payment account to verify the gas payment looks right. + let gas_payment_account_data = banks_client + .get_account(gas_payment_pda_key) + .await + .unwrap() + .unwrap() + .data; + let gas_payment = GasPaymentAccount::fetch(&mut &gas_payment_account_data[..]) + .unwrap() + .into_inner(); + + assert_eq!( + *gas_payment, + GasPaymentData { + sequence_number: 0, + igp: igp_accounts.igp, + destination_domain: REMOTE_DOMAIN, + message_id: message.id(), + gas_amount: REMOTE_GAS_AMOUNT, + unique_gas_payment_pubkey: unique_message_account_keypair.pubkey(), + slot: transfer_remote_tx_status.slot, + payment: REMOTE_GAS_AMOUNT + } + .into(), + ); +} + +#[tokio::test] +async fn test_enroll_remote_router() { + let program_id = hyperlane_sealevel_token_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + // Verify the remote router was enrolled. + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!( + token.remote_routers, + vec![(REMOTE_DOMAIN, remote_router)].into_iter().collect(), + ); +} + +#[tokio::test] +async fn test_enroll_remote_router_errors_if_not_signed_by_owner() { + let program_id = hyperlane_sealevel_token_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + let non_owner = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + + // Use the non_owner as the payer, which has a balance but is not the owner, + // so we expect this to fail. + let result = enroll_remote_router( + &mut banks_client, + &program_id, + &non_owner, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + H256::random(), + ) + .await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); + + // Also try using the non_owner as the payer and specifying the correct + // owner account, but the owner isn't a signer: + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Enroll the remote router + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::EnrollRemoteRouter(RemoteRouterConfig { + domain: REMOTE_DOMAIN, + router: Some(H256::random()), + }) + .encode() + .unwrap(), + vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), false), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature), + ); +} + +#[tokio::test] +async fn test_set_destination_gas_configs() { + let program_id = hyperlane_sealevel_token_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + // Set the destination gas config + let gas = 111222333; + set_destination_gas_config( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + gas, + ) + .await + .unwrap(); + + // Verify the destination gas was set. + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!( + token.destination_gas, + vec![(REMOTE_DOMAIN, gas)].into_iter().collect(), + ); +} + +#[tokio::test] +async fn test_set_destination_gas_configs_errors_if_not_signed_by_owner() { + let program_id = hyperlane_sealevel_token_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + let non_owner = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + + // Use the non_owner as the payer, which has a balance but is not the owner, + // so we expect this to fail. + let gas = 111222333; + let result = set_destination_gas_config( + &mut banks_client, + &program_id, + &non_owner, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + gas, + ) + .await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); + + // Also try using the non_owner as the payer and specifying the correct + // owner account, but the owner isn't a signer: + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Try setting + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetDestinationGasConfigs(vec![GasRouterConfig { + domain: REMOTE_DOMAIN, + gas: Some(gas), + }]) + .encode() + .unwrap(), + vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), false), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature), + ); +} + +#[tokio::test] +async fn test_transfer_ownership() { + let program_id = hyperlane_sealevel_token_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + let new_owner = Some(Pubkey::new_unique()); + + // Transfer ownership + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::TransferOwnership(new_owner) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + // Verify the new owner is set + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!(token.owner, new_owner); +} + +#[tokio::test] +async fn test_transfer_ownership_errors_if_owner_not_signer() { + let program_id = hyperlane_sealevel_token_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + let new_owner = Some(Pubkey::new_unique()); + let non_owner = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + + // Try transferring ownership using the mint authority key + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::TransferOwnership(new_owner) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(non_owner.pubkey(), true), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); +} + +#[tokio::test] +async fn test_set_interchain_security_module() { + let program_id = hyperlane_sealevel_token_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + let new_ism = Some(Pubkey::new_unique()); + + // Set the ISM + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainSecurityModule(new_ism) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + // Verify the new ISM is set + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!(token.interchain_security_module, new_ism); +} + +#[tokio::test] +async fn test_set_interchain_security_module_errors_if_owner_not_signer() { + let program_id = hyperlane_sealevel_token_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + let new_ism = Some(Pubkey::new_unique()); + let non_owner = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + + // Try setting the ISM using the mint authority key + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainSecurityModule(new_ism) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(non_owner.pubkey(), true), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); + + // Also try using the non_owner as the payer and specifying the correct + // owner account, but the owner isn't a signer: + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainSecurityModule(new_ism) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), false), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature), + ); +} + +#[tokio::test] +async fn test_set_interchain_gas_paymaster() { + let program_id = hyperlane_sealevel_token_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + let new_igp = Some(( + Pubkey::new_unique(), + InterchainGasPaymasterType::OverheadIgp(Pubkey::new_unique()), + )); + + // Set the IGP + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainGasPaymaster(new_igp.clone()) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + // Verify the new IGP is set + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!(token.interchain_gas_paymaster, new_igp); +} + +#[tokio::test] +async fn test_set_interchain_gas_paymaster_errors_if_owner_not_signer() { + let program_id = hyperlane_sealevel_token_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + let new_igp = Some(( + Pubkey::new_unique(), + InterchainGasPaymasterType::OverheadIgp(Pubkey::new_unique()), + )); + let non_owner = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + + // Try setting the ISM using the mint authority key + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainGasPaymaster(new_igp.clone()) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(non_owner.pubkey(), true), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); + + // Also try using the non_owner as the payer and specifying the correct + // owner account, but the owner isn't a signer: + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainGasPaymaster(new_igp) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), false), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature), + ); +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-native-memo/Cargo.toml b/rust/sealevel/programs/hyperlane-sealevel-token-native-memo/Cargo.toml new file mode 100644 index 00000000000..a920d3e6158 --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-native-memo/Cargo.toml @@ -0,0 +1,47 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "hyperlane-sealevel-token-native-memo" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] + +[dependencies] +borsh.workspace = true +num-derive.workspace = true +num-traits.workspace = true +solana-program.workspace = true +spl-noop.workspace = true +thiserror.workspace = true + +account-utils = { path = "../../libraries/account-utils" } +hyperlane-core = { path = "../../../main/hyperlane-core" } +hyperlane-sealevel-connection-client = { path = "../../libraries/hyperlane-sealevel-connection-client" } +hyperlane-sealevel-mailbox = { path = "../mailbox", features = [ + "no-entrypoint", +] } +hyperlane-sealevel-igp = { path = "../hyperlane-sealevel-igp", features = [ + "no-entrypoint", +] } +hyperlane-sealevel-message-recipient-interface = { path = "../../libraries/message-recipient-interface" } +hyperlane-sealevel-token-lib = { path = "../../libraries/hyperlane-sealevel-token" } +hyperlane-warp-route = { path = "../../../main/applications/hyperlane-warp-route" } +serializable-account-meta = { path = "../../libraries/serializable-account-meta" } +hyperlane-sealevel-token-native = { path = "../hyperlane-sealevel-token-native" } + +[dev-dependencies] +solana-program-test.workspace = true +solana-sdk.workspace = true + +hyperlane-test-utils = { path = "../../libraries/test-utils" } +hyperlane-sealevel-test-ism = { path = "../ism/test-ism", features = [ + "no-entrypoint", +] } +# Unfortunately required for some functions in `solana-program-test`, and is not +# re-exported +tarpc = "~0.29" + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-native-memo/src/instruction.rs b/rust/sealevel/programs/hyperlane-sealevel-token-native-memo/src/instruction.rs new file mode 100644 index 00000000000..cd51385a252 --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-native-memo/src/instruction.rs @@ -0,0 +1,32 @@ +//! Instructions for the program. + +use crate::hyperlane_token_native_collateral_pda_seeds; + +use hyperlane_sealevel_token_lib::instruction::{init_instruction as lib_init_instruction, Init}; + +use solana_program::{ + instruction::{AccountMeta, Instruction as SolanaInstruction}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +/// Gets an instruction to initialize the program. +pub fn init_instruction( + program_id: Pubkey, + payer: Pubkey, + init: Init, +) -> Result { + let mut instruction = lib_init_instruction(program_id, payer, init)?; + + // Add additional account metas: + // 0. `[writable]` The native collateral PDA account. + + let (native_collateral_key, _native_collatera_bump) = + Pubkey::find_program_address(hyperlane_token_native_collateral_pda_seeds!(), &program_id); + + instruction + .accounts + .append(&mut vec![AccountMeta::new(native_collateral_key, false)]); + + Ok(instruction) +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-native-memo/src/lib.rs b/rust/sealevel/programs/hyperlane-sealevel-token-native-memo/src/lib.rs new file mode 100644 index 00000000000..78ec3b2d339 --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-native-memo/src/lib.rs @@ -0,0 +1,11 @@ +//! Hyperlane token program for native tokens. + +#![deny(warnings)] +#![deny(missing_docs)] +#![deny(unsafe_code)] + +pub mod instruction; +pub mod plugin; +pub mod processor; + +pub use spl_noop; diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-native-memo/src/plugin.rs b/rust/sealevel/programs/hyperlane-sealevel-token-native-memo/src/plugin.rs new file mode 100644 index 00000000000..cac54e4b643 --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-native-memo/src/plugin.rs @@ -0,0 +1,207 @@ +//! A plugin for the Hyperlane token program that transfers native +//! tokens in from a sender when sending to a remote chain, and transfers +//! native tokens out to recipients when receiving from a remote chain. + +use account_utils::{create_pda_account, verify_rent_exempt, SizedData}; +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_sealevel_token_lib::{ + accounts::HyperlaneToken, processor::HyperlaneSealevelTokenPlugin, +}; +use hyperlane_warp_route::TokenMessage; +use serializable_account_meta::SerializableAccountMeta; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + instruction::AccountMeta, + program::{invoke, invoke_signed}, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, + system_instruction, + sysvar::Sysvar, +}; + +/// Seeds relating to the PDA account that holds native collateral. +#[macro_export] +macro_rules! hyperlane_token_native_collateral_pda_seeds { + () => {{ + &[b"hyperlane_token", b"-", b"native_collateral"] + }}; + + ($bump_seed:expr) => {{ + &[ + b"hyperlane_token", + b"-", + b"native_collateral", + &[$bump_seed], + ] + }}; +} + +/// A plugin for the Hyperlane token program that transfers native +/// tokens in from a sender when sending to a remote chain, and transfers +/// native tokens out to recipients when receiving from a remote chain. +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq, Default)] +pub struct NativePlugin { + /// The bump seed for the native collateral PDA account. + pub native_collateral_bump: u8, +} + +impl SizedData for NativePlugin { + fn size(&self) -> usize { + // native_collateral_bump + std::mem::size_of::() + } +} + +impl NativePlugin { + /// Returns Ok(()) if the native collateral account info is valid. + /// Errors if the key or owner is incorrect. + fn verify_native_collateral_account_info( + program_id: &Pubkey, + token: &HyperlaneToken, + native_collateral_account_info: &AccountInfo, + ) -> Result<(), ProgramError> { + let native_collateral_seeds: &[&[u8]] = + hyperlane_token_native_collateral_pda_seeds!(token.plugin_data.native_collateral_bump); + let expected_native_collateral_key = + Pubkey::create_program_address(native_collateral_seeds, program_id)?; + + if native_collateral_account_info.key != &expected_native_collateral_key { + return Err(ProgramError::InvalidArgument); + } + Ok(()) + } +} + +impl HyperlaneSealevelTokenPlugin for NativePlugin { + /// Initializes the plugin. + /// + /// Accounts: + /// 0. `[writable]` The native collateral PDA account. + fn initialize<'a, 'b>( + program_id: &Pubkey, + system_program: &'a AccountInfo<'b>, + _token_account: &'a AccountInfo<'b>, + payer_account: &'a AccountInfo<'b>, + accounts_iter: &mut std::slice::Iter<'a, AccountInfo<'b>>, + ) -> Result { + // Account 0: Native collateral PDA account. + let native_collateral_account = next_account_info(accounts_iter)?; + let (native_collateral_key, native_collateral_bump) = Pubkey::find_program_address( + hyperlane_token_native_collateral_pda_seeds!(), + program_id, + ); + if &native_collateral_key != native_collateral_account.key { + return Err(ProgramError::InvalidArgument); + } + + // Create native collateral PDA account. + // Assign ownership to the system program so it can transfer tokens. + create_pda_account( + payer_account, + &Rent::get()?, + 0, + &solana_program::system_program::id(), + system_program, + native_collateral_account, + hyperlane_token_native_collateral_pda_seeds!(native_collateral_bump), + )?; + + Ok(Self { + native_collateral_bump, + }) + } + + /// Transfers tokens into the program so they can be sent to a remote chain. + /// Burns the tokens from the sender's associated token account. + /// + /// Accounts: + /// 0. `[executable]` The system program. + /// 1. `[writeable]` The native token collateral PDA account. + fn transfer_in<'a, 'b>( + program_id: &Pubkey, + token: &HyperlaneToken, + sender_wallet: &'a AccountInfo<'b>, + accounts_iter: &mut std::slice::Iter<'a, AccountInfo<'b>>, + amount: u64, + ) -> Result<(), ProgramError> { + // Account 0: System program. + let system_program = next_account_info(accounts_iter)?; + if system_program.key != &solana_program::system_program::id() { + return Err(ProgramError::InvalidArgument); + } + + // Account 1: Native collateral PDA account. + let native_collateral_account = next_account_info(accounts_iter)?; + Self::verify_native_collateral_account_info(program_id, token, native_collateral_account)?; + + // Transfer tokens into the native collateral account. + invoke( + &system_instruction::transfer(sender_wallet.key, native_collateral_account.key, amount), + &[sender_wallet.clone(), native_collateral_account.clone()], + ) + } + + /// Transfers tokens out to a recipient's associated token account as a + /// result of a transfer to this chain from a remote chain. + /// + /// Accounts: + /// 0. `[executable]` The system program. + /// 1. `[writeable]` The native token collateral PDA account. + fn transfer_out<'a, 'b>( + program_id: &Pubkey, + token: &HyperlaneToken, + _system_program: &'a AccountInfo<'b>, + recipient_wallet: &'a AccountInfo<'b>, + accounts_iter: &mut std::slice::Iter<'a, AccountInfo<'b>>, + amount: u64, + ) -> Result<(), ProgramError> { + // Account 0: System program. + let system_program = next_account_info(accounts_iter)?; + if system_program.key != &solana_program::system_program::id() { + return Err(ProgramError::InvalidArgument); + } + + // Account 1: Native collateral PDA account. + let native_collateral_account = next_account_info(accounts_iter)?; + Self::verify_native_collateral_account_info(program_id, token, native_collateral_account)?; + + invoke_signed( + &system_instruction::transfer( + native_collateral_account.key, + recipient_wallet.key, + amount, + ), + &[native_collateral_account.clone(), recipient_wallet.clone()], + &[hyperlane_token_native_collateral_pda_seeds!( + token.plugin_data.native_collateral_bump + )], + )?; + + // Ensure the native collateral account is still rent exempt. + verify_rent_exempt(native_collateral_account, &Rent::get()?)?; + + Ok(()) + } + + /// Returns the accounts required for `transfer_out`. + fn transfer_out_account_metas( + program_id: &Pubkey, + _token: &HyperlaneToken, + _token_message: &TokenMessage, + ) -> Result<(Vec, bool), ProgramError> { + let (native_collateral_key, _native_collateral_bump) = Pubkey::find_program_address( + hyperlane_token_native_collateral_pda_seeds!(), + program_id, + ); + + Ok(( + vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false).into(), + AccountMeta::new(native_collateral_key, false).into(), + ], + // Recipient wallet must be writeable to send lamports to it. + true, + )) + } +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-native-memo/src/processor.rs b/rust/sealevel/programs/hyperlane-sealevel-token-native-memo/src/processor.rs new file mode 100644 index 00000000000..3ec47f4c06a --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-native-memo/src/processor.rs @@ -0,0 +1,286 @@ +//! Program processor. + +use account_utils::DiscriminatorDecode; +use hyperlane_sealevel_connection_client::{ + gas_router::GasRouterConfig, router::RemoteRouterConfig, +}; +use hyperlane_sealevel_igp::accounts::InterchainGasPaymasterType; +use hyperlane_sealevel_message_recipient_interface::{ + HandleInstruction, MessageRecipientInstruction, +}; +use hyperlane_sealevel_token_lib::{ + instruction::{ + DymInstruction, Init, Instruction as TokenIxn, TransferRemote, TransferRemoteMemo, + }, + processor::HyperlaneSealevelToken, +}; +use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, msg, pubkey::Pubkey}; + +use crate::plugin::NativePlugin; + +#[cfg(not(feature = "no-entrypoint"))] +solana_program::entrypoint!(process_instruction); + +/// Processes an instruction. +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + // First, check if the instruction has a discriminant relating to + // the message recipient interface. + if let Ok(message_recipient_instruction) = MessageRecipientInstruction::decode(instruction_data) + { + return match message_recipient_instruction { + MessageRecipientInstruction::InterchainSecurityModule => { + interchain_security_module(program_id, accounts) + } + MessageRecipientInstruction::InterchainSecurityModuleAccountMetas => { + interchain_security_module_account_metas(program_id) + } + MessageRecipientInstruction::Handle(handle) => transfer_from_remote( + program_id, + accounts, + HandleInstruction { + origin: handle.origin, + sender: handle.sender, + message: handle.message, + }, + ), + MessageRecipientInstruction::HandleAccountMetas(handle) => { + transfer_from_remote_account_metas( + program_id, + accounts, + HandleInstruction { + origin: handle.origin, + sender: handle.sender, + message: handle.message, + }, + ) + } + }; + } + + if let Ok(instr) = DymInstruction::decode(instruction_data) { + return match instr { + DymInstruction::TransferRemoteMemo(xfer) => { + transfer_remote_memo(program_id, accounts, xfer) + } + } + .map_err(|err| { + msg!("{}", err); + err + }); + } + + // Otherwise, try decoding a "normal" token instruction + match TokenIxn::decode(instruction_data)? { + TokenIxn::Init(init) => initialize(program_id, accounts, init), + TokenIxn::TransferRemote(xfer) => transfer_remote(program_id, accounts, xfer), + TokenIxn::EnrollRemoteRouter(config) => enroll_remote_router(program_id, accounts, config), + TokenIxn::EnrollRemoteRouters(configs) => { + enroll_remote_routers(program_id, accounts, configs) + } + TokenIxn::SetDestinationGasConfigs(configs) => { + set_destination_gas_configs(program_id, accounts, configs) + } + TokenIxn::TransferOwnership(new_owner) => { + transfer_ownership(program_id, accounts, new_owner) + } + TokenIxn::SetInterchainSecurityModule(new_ism) => { + set_interchain_security_module(program_id, accounts, new_ism) + } + TokenIxn::SetInterchainGasPaymaster(new_igp) => { + set_interchain_gas_paymaster(program_id, accounts, new_igp) + } + } + .map_err(|err| { + msg!("{}", err); + err + }) +} + +fn transfer_remote_memo( + program_id: &Pubkey, + accounts: &[AccountInfo], + transfer: TransferRemoteMemo, +) -> ProgramResult { + let base = transfer.base; + let memo = transfer.memo; + HyperlaneSealevelToken::::transfer_remote_memo(program_id, accounts, base, memo) +} + +/// Initializes the program. +/// +/// Accounts: +/// 0. `[executable]` The system program. +/// 1. `[writable]` The token PDA account. +/// 2. `[writable]` The dispatch authority PDA account. +/// 3. `[signer]` The payer and mailbox payer. +/// 4. `[writable]` The native collateral PDA account. +fn initialize(program_id: &Pubkey, accounts: &[AccountInfo], init: Init) -> ProgramResult { + HyperlaneSealevelToken::::initialize(program_id, accounts, init) +} + +/// Transfers tokens to a remote. +/// Transfers the native lamports into the native token collateral PDA account and +/// then dispatches a message to the remote recipient. +/// +/// Accounts: +/// 0. `[executable]` The system program. +/// 1. `[executable]` The spl_noop program. +/// 2. `[]` The token PDA account. +/// 3. `[executable]` The mailbox program. +/// 4. `[writeable]` The mailbox outbox account. +/// 5. `[]` Message dispatch authority. +/// 6. `[signer]` The token sender and mailbox payer. +/// 7. `[signer]` Unique message / gas payment account. +/// 8. `[writeable]` Message storage PDA. +/// ---- If using an IGP ---- +/// 9. `[executable]` The IGP program. +/// 10. `[writeable]` The IGP program data. +/// 11. `[writeable]` Gas payment PDA. +/// 12. `[]` OPTIONAL - The Overhead IGP program, if the configured IGP is an Overhead IGP. +/// 13. `[writeable]` The IGP account. +/// ---- End if ---- +/// 14. `[executable]` The system program. +/// 15. `[writeable]` The native token collateral PDA account. +fn transfer_remote( + program_id: &Pubkey, + accounts: &[AccountInfo], + transfer: TransferRemote, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_remote(program_id, accounts, transfer) +} + +/// Accounts: +/// 0. `[signer]` Mailbox processor authority specific to this program. +/// 1. `[executable]` system_program +/// 2. `[]` hyperlane_token storage +/// 3. `[writeable]` recipient wallet address +/// 4. `[executable]` The system program. +/// 5. `[writeable]` The native token collateral PDA account. +fn transfer_from_remote( + program_id: &Pubkey, + accounts: &[AccountInfo], + transfer: HandleInstruction, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_from_remote(program_id, accounts, transfer) +} + +/// Gets the account metas for a `transfer_from_remote` instruction. +/// +/// Accounts: +/// None +fn transfer_from_remote_account_metas( + program_id: &Pubkey, + accounts: &[AccountInfo], + transfer: HandleInstruction, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_from_remote_account_metas( + program_id, accounts, transfer, + ) +} + +/// Enrolls a remote router. +/// +/// Accounts: +/// 0. `[executable]` The system program. +/// 1. `[writeable]` The token PDA account. +/// 2. `[signer]` The owner. +fn enroll_remote_router( + program_id: &Pubkey, + accounts: &[AccountInfo], + config: RemoteRouterConfig, +) -> ProgramResult { + HyperlaneSealevelToken::::enroll_remote_router(program_id, accounts, config) +} + +/// Enrolls remote routers. +/// +/// Accounts: +/// 0. `[executable]` The system program. +/// 1. `[writeable]` The token PDA account. +/// 2. `[signer]` The owner. +fn enroll_remote_routers( + program_id: &Pubkey, + accounts: &[AccountInfo], + configs: Vec, +) -> ProgramResult { + HyperlaneSealevelToken::::enroll_remote_routers(program_id, accounts, configs) +} + +/// Sets the destination gas configs. +/// +/// Accounts: +/// 0. `[executable]` The system program. +/// 1. `[writeable]` The token PDA account. +/// 2. `[signer]` The owner. +fn set_destination_gas_configs( + program_id: &Pubkey, + accounts: &[AccountInfo], + configs: Vec, +) -> ProgramResult { + HyperlaneSealevelToken::::set_destination_gas_configs( + program_id, accounts, configs, + ) +} + +/// Transfers ownership. +/// +/// Accounts: +/// 0. `[writeable]` The token PDA account. +/// 1. `[signer]` The current owner. +fn transfer_ownership( + program_id: &Pubkey, + accounts: &[AccountInfo], + new_owner: Option, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_ownership(program_id, accounts, new_owner) +} + +/// Gets the interchain security module, returning it as a serialized Option. +/// +/// Accounts: +/// 0. `[]` The token PDA account. +fn interchain_security_module(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + HyperlaneSealevelToken::::interchain_security_module(program_id, accounts) +} + +/// Gets the account metas for getting the interchain security module. +/// +/// Accounts: +/// None +fn interchain_security_module_account_metas(program_id: &Pubkey) -> ProgramResult { + HyperlaneSealevelToken::::interchain_security_module_account_metas(program_id) +} + +/// Lets the owner set the interchain security module. +/// +/// Accounts: +/// 0. `[writeable]` The token PDA account. +/// 1. `[signer]` The access control owner. +fn set_interchain_security_module( + program_id: &Pubkey, + accounts: &[AccountInfo], + new_ism: Option, +) -> ProgramResult { + HyperlaneSealevelToken::::set_interchain_security_module( + program_id, accounts, new_ism, + ) +} + +/// Lets the owner set the interchain gas paymaster. +/// +/// Accounts: +/// 0. `[writeable]` The token PDA account. +/// 1. `[signer]` The access control owner. +fn set_interchain_gas_paymaster( + program_id: &Pubkey, + accounts: &[AccountInfo], + new_igp: Option<(Pubkey, InterchainGasPaymasterType)>, +) -> ProgramResult { + HyperlaneSealevelToken::::set_interchain_gas_paymaster( + program_id, accounts, new_igp, + ) +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-native-memo/tests/functional.rs b/rust/sealevel/programs/hyperlane-sealevel-token-native-memo/tests/functional.rs new file mode 100644 index 00000000000..0b210b6d704 --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-native-memo/tests/functional.rs @@ -0,0 +1,1333 @@ +//! Contains functional tests for things that cannot be done +//! strictly in unit tests. This includes CPIs, like creating +//! new PDA accounts. + +use account_utils::DiscriminatorEncode; +use hyperlane_core::{Encode, HyperlaneMessage, H256, U256}; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey, + pubkey::Pubkey, +}; +use std::collections::HashMap; + +use hyperlane_sealevel_connection_client::{ + gas_router::GasRouterConfig, router::RemoteRouterConfig, +}; +use hyperlane_sealevel_igp::{ + accounts::{GasPaymentAccount, GasPaymentData, InterchainGasPaymasterType}, + igp_gas_payment_pda_seeds, +}; +use hyperlane_sealevel_mailbox::{ + accounts::{DispatchedMessage, DispatchedMessageAccount}, + mailbox_dispatched_message_pda_seeds, mailbox_message_dispatch_authority_pda_seeds, + mailbox_process_authority_pda_seeds, + protocol_fee::ProtocolFee, +}; +use hyperlane_sealevel_message_recipient_interface::{ + HandleInstruction, MessageRecipientInstruction, +}; +use hyperlane_sealevel_token_lib::{ + accounts::{convert_decimals, HyperlaneToken, HyperlaneTokenAccount}, + hyperlane_token_pda_seeds, + instruction::{ + DymInstruction, Init, Instruction as HyperlaneTokenInstruction, TransferRemote, + TransferRemoteMemo, + }, +}; +use hyperlane_sealevel_token_native::{ + hyperlane_token_native_collateral_pda_seeds, plugin::NativePlugin, +}; +use hyperlane_sealevel_token_native_memo::processor::process_instruction; +use hyperlane_test_utils::{ + assert_lamports, assert_transaction_error, igp_program_id, initialize_igp_accounts, + initialize_mailbox, mailbox_id, new_funded_keypair, process, transfer_lamports, IgpAccounts, +}; +use hyperlane_warp_route::TokenMessage; +use solana_program_test::*; +use solana_sdk::{ + commitment_config::CommitmentLevel, + instruction::InstructionError, + signature::Signer, + signer::keypair::Keypair, + transaction::{Transaction, TransactionError}, +}; +use tarpc::context::Context; + +/// There are 1e9 lamports in one SOL. +const ONE_SOL_IN_LAMPORTS: u64 = 1000000000; +const LOCAL_DOMAIN: u32 = 1234; +const LOCAL_DECIMALS: u8 = 9; +const LOCAL_DECIMALS_U32: u32 = LOCAL_DECIMALS as u32; +const REMOTE_DOMAIN: u32 = 4321; +const REMOTE_DECIMALS: u8 = 18; +const REMOTE_GAS_AMOUNT: u64 = 200000; + +fn hyperlane_sealevel_token_native_id() -> Pubkey { + pubkey!("CGn8yNtSD3aTTqJfYhUb6s1aVTN75NzwtsFKo1e83aga") +} + +async fn setup_client() -> (BanksClient, Keypair) { + let program_id = hyperlane_sealevel_token_native_id(); + let mut program_test = ProgramTest::new( + "hyperlane_sealevel_token_native", + program_id, + processor!(process_instruction), + ); + + program_test.add_program("spl_noop", spl_noop::id(), processor!(spl_noop::noop)); + + let mailbox_program_id = mailbox_id(); + program_test.add_program( + "hyperlane_sealevel_mailbox", + mailbox_program_id, + processor!(hyperlane_sealevel_mailbox::processor::process_instruction), + ); + + program_test.add_program( + "hyperlane_sealevel_igp", + igp_program_id(), + processor!(hyperlane_sealevel_igp::processor::process_instruction), + ); + + // This serves as the default ISM on the Mailbox + program_test.add_program( + "hyperlane_sealevel_test_ism", + hyperlane_sealevel_test_ism::id(), + processor!(hyperlane_sealevel_test_ism::program::process_instruction), + ); + + let (banks_client, payer, _recent_blockhash) = program_test.start().await; + + (banks_client, payer) +} + +struct HyperlaneTokenAccounts { + token: Pubkey, + token_bump: u8, + mailbox_process_authority: Pubkey, + dispatch_authority: Pubkey, + dispatch_authority_bump: u8, + native_collateral: Pubkey, + native_collateral_bump: u8, +} + +async fn initialize_hyperlane_token( + program_id: &Pubkey, + banks_client: &mut BanksClient, + payer: &Keypair, + igp_accounts: Option<&IgpAccounts>, +) -> Result { + let (mailbox_process_authority_key, _mailbox_process_authority_bump) = + Pubkey::find_program_address( + mailbox_process_authority_pda_seeds!(program_id), + &mailbox_id(), + ); + + let (token_account_key, token_account_bump_seed) = + Pubkey::find_program_address(hyperlane_token_pda_seeds!(), program_id); + + let (dispatch_authority_key, dispatch_authority_seed) = + Pubkey::find_program_address(mailbox_message_dispatch_authority_pda_seeds!(), program_id); + + let (native_collateral_account_key, native_collateral_account_bump_seed) = + Pubkey::find_program_address(hyperlane_token_native_collateral_pda_seeds!(), program_id); + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + *program_id, + &HyperlaneTokenInstruction::Init(Init { + mailbox: mailbox_id(), + interchain_security_module: None, + interchain_gas_paymaster: igp_accounts.map(|igp_accounts| { + ( + igp_accounts.program, + InterchainGasPaymasterType::OverheadIgp(igp_accounts.overhead_igp), + ) + }), + decimals: LOCAL_DECIMALS, + remote_decimals: REMOTE_DECIMALS, + }) + .encode() + .unwrap(), + vec![ + // 0. `[executable]` The system program. + // 1. `[writable]` The token PDA account. + // 2. `[writable]` The dispatch authority PDA account. + // 3. `[signer]` The payer and mailbox payer. + // 4. `[writable]` The native collateral PDA account. + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(token_account_key, false), + AccountMeta::new(dispatch_authority_key, false), + AccountMeta::new_readonly(payer.pubkey(), true), + AccountMeta::new(native_collateral_account_key, false), + ], + )], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await?; + + // Set destination gas configs + set_destination_gas_config( + banks_client, + program_id, + payer, + &token_account_key, + REMOTE_DOMAIN, + REMOTE_GAS_AMOUNT, + ) + .await?; + + Ok(HyperlaneTokenAccounts { + token: token_account_key, + token_bump: token_account_bump_seed, + mailbox_process_authority: mailbox_process_authority_key, + dispatch_authority: dispatch_authority_key, + dispatch_authority_bump: dispatch_authority_seed, + native_collateral: native_collateral_account_key, + native_collateral_bump: native_collateral_account_bump_seed, + }) +} + +async fn enroll_remote_router( + banks_client: &mut BanksClient, + program_id: &Pubkey, + payer: &Keypair, + token_account: &Pubkey, + domain: u32, + router: H256, +) -> Result<(), BanksClientError> { + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Enroll the remote router + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + *program_id, + &HyperlaneTokenInstruction::EnrollRemoteRouter(RemoteRouterConfig { + domain, + router: Some(router), + }) + .encode() + .unwrap(), + vec![ + AccountMeta::new(solana_program::system_program::id(), false), + AccountMeta::new(*token_account, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await?; + + Ok(()) +} + +async fn set_destination_gas_config( + banks_client: &mut BanksClient, + program_id: &Pubkey, + payer: &Keypair, + token_account: &Pubkey, + domain: u32, + gas: u64, +) -> Result<(), BanksClientError> { + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Enroll the remote router + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + *program_id, + &HyperlaneTokenInstruction::SetDestinationGasConfigs(vec![GasRouterConfig { + domain, + gas: Some(gas), + }]) + .encode() + .unwrap(), + vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(*token_account, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await?; + + Ok(()) +} + +#[tokio::test] +async fn test_initialize() { + let program_id = hyperlane_sealevel_token_native_id(); + let mailbox_program_id = mailbox_id(); + + let (mut banks_client, payer) = setup_client().await; + + let mailbox_accounts = initialize_mailbox( + &mut banks_client, + &mailbox_program_id, + &payer, + LOCAL_DOMAIN, + ONE_SOL_IN_LAMPORTS, + ProtocolFee::default(), + ) + .await + .unwrap(); + + let igp_accounts = + initialize_igp_accounts(&mut banks_client, &igp_program_id(), &payer, REMOTE_DOMAIN) + .await + .unwrap(); + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, Some(&igp_accounts)) + .await + .unwrap(); + + // Get the token account. + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!( + token, + Box::new(HyperlaneToken { + bump: hyperlane_token_accounts.token_bump, + mailbox: mailbox_accounts.program, + mailbox_process_authority: hyperlane_token_accounts.mailbox_process_authority, + dispatch_authority_bump: hyperlane_token_accounts.dispatch_authority_bump, + decimals: LOCAL_DECIMALS, + remote_decimals: REMOTE_DECIMALS, + owner: Some(payer.pubkey()), + interchain_security_module: None, + interchain_gas_paymaster: Some(( + igp_accounts.program, + InterchainGasPaymasterType::OverheadIgp(igp_accounts.overhead_igp), + )), + destination_gas: HashMap::from([(REMOTE_DOMAIN, REMOTE_GAS_AMOUNT)]), + remote_routers: HashMap::new(), + plugin_data: NativePlugin { + native_collateral_bump: hyperlane_token_accounts.native_collateral_bump, + }, + }), + ); + + // Verify the ATA payer account was created. + let native_collateral_account = banks_client + .get_account(hyperlane_token_accounts.native_collateral) + .await + .unwrap() + .unwrap(); + assert!(native_collateral_account.lamports > 0); +} + +#[tokio::test] +async fn test_initialize_errors_if_called_twice() { + let program_id = hyperlane_sealevel_token_native_id(); + + let (mut banks_client, payer) = setup_client().await; + + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + let other_payer = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + + // To ensure a different signature is used, we'll use a different payer + let init_result = + initialize_hyperlane_token(&program_id, &mut banks_client, &other_payer, None).await; + + assert_transaction_error( + init_result, + TransactionError::InstructionError(0, InstructionError::AccountAlreadyInitialized), + ); +} + +#[tokio::test] +async fn test_transfer_remote_memo() { + let program_id = hyperlane_sealevel_token_native_id(); + let mailbox_program_id = mailbox_id(); + + let (mut banks_client, payer) = setup_client().await; + + let mailbox_accounts = initialize_mailbox( + &mut banks_client, + &mailbox_program_id, + &payer, + LOCAL_DOMAIN, + ONE_SOL_IN_LAMPORTS, + ProtocolFee::default(), + ) + .await + .unwrap(); + + let igp_accounts = + initialize_igp_accounts(&mut banks_client, &igp_program_id(), &payer, REMOTE_DOMAIN) + .await + .unwrap(); + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, Some(&igp_accounts)) + .await + .unwrap(); + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + // Send 100 SOL for the token sender to start with. + let token_sender = + new_funded_keypair(&mut banks_client, &payer, 100 * ONE_SOL_IN_LAMPORTS).await; + let token_sender_pubkey = token_sender.pubkey(); + + // Call transfer_remote + let unique_message_account_keypair = Keypair::new(); + let (dispatched_message_key, _dispatched_message_bump) = Pubkey::find_program_address( + mailbox_dispatched_message_pda_seeds!(&unique_message_account_keypair.pubkey()), + &mailbox_program_id, + ); + let (gas_payment_pda_key, _gas_payment_pda_bump) = Pubkey::find_program_address( + igp_gas_payment_pda_seeds!(&unique_message_account_keypair.pubkey()), + &igp_program_id(), + ); + + let remote_token_recipient = H256::random(); + // Transfer 69 tokens. + let transfer_amount = 69 * ONE_SOL_IN_LAMPORTS; + let remote_transfer_amount = + convert_decimals(transfer_amount.into(), LOCAL_DECIMALS, REMOTE_DECIMALS).unwrap(); + + let sender_balance_before = banks_client.get_balance(token_sender_pubkey).await.unwrap(); + let native_collateral_account_lamports_before = banks_client + .get_balance(hyperlane_token_accounts.native_collateral) + .await + .unwrap(); + + let test_memo = vec![1, 2, 3]; + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &DymInstruction::TransferRemoteMemo(TransferRemoteMemo { + base: TransferRemote { + destination_domain: REMOTE_DOMAIN, + recipient: remote_token_recipient, + amount_or_id: transfer_amount.into(), + }, + memo: test_memo.clone(), + }) + .encode() + .unwrap(), + // 0. `[executable]` The system program. + // 1. `[executable]` The spl_noop program. + // 2. `[]` The token PDA account. + // 3. `[executable]` The mailbox program. + // 4. `[writeable]` The mailbox outbox account. + // 5. `[]` Message dispatch authority. + // 6. `[signer]` The token sender and mailbox payer. + // 7. `[signer]` Unique message / gas payment account. + // 8. `[writeable]` Message storage PDA. + // ---- If using an IGP ---- + // 9. `[executable]` The IGP program. + // 10. `[writeable]` The IGP program data. + // 11. `[writeable]` Gas payment PDA. + // 12. `[]` OPTIONAL - The Overhead IGP program, if the configured IGP is an Overhead IGP. + // 13. `[writeable]` The IGP account. + // ---- End if ---- + // 14. `[executable]` The system program. + // 15. `[writeable]` The native token collateral PDA account. + vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new_readonly(spl_noop::id(), false), + AccountMeta::new_readonly(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(mailbox_accounts.program, false), + AccountMeta::new(mailbox_accounts.outbox, false), + AccountMeta::new_readonly(hyperlane_token_accounts.dispatch_authority, false), + AccountMeta::new_readonly(token_sender_pubkey, true), + AccountMeta::new_readonly(unique_message_account_keypair.pubkey(), true), + AccountMeta::new(dispatched_message_key, false), + AccountMeta::new_readonly(igp_accounts.program, false), + AccountMeta::new(igp_accounts.program_data, false), + AccountMeta::new(gas_payment_pda_key, false), + AccountMeta::new_readonly(igp_accounts.overhead_igp, false), + AccountMeta::new(igp_accounts.igp, false), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(hyperlane_token_accounts.native_collateral, false), + ], + )], + Some(&token_sender_pubkey), + &[&token_sender, &unique_message_account_keypair], + recent_blockhash, + ); + + let transaction_fee = banks_client + .get_fee_for_message_with_commitment_and_context( + Context::current(), + CommitmentLevel::Processed, + transaction.message.clone(), + ) + .await + .unwrap() + .unwrap(); + + let tx_signature = transaction.signatures[0]; + banks_client.process_transaction(transaction).await.unwrap(); + + // The transaction fee doesn't seem to be entirely accurate - + // this may be due to a mismatch between the SDK and the actual + // transaction fee calculation. + // For now, we'll just check that the sender's balance roughly correct. + let sender_balance_after = banks_client.get_balance(token_sender_pubkey).await.unwrap(); + let expected_balance_after = sender_balance_before - transfer_amount - transaction_fee; + // Allow 0.005 SOL of extra transaction fees + assert!( + sender_balance_after >= expected_balance_after - 5000000 + && sender_balance_after <= expected_balance_after + ); + + // And that the native collateral account's balance is 69 tokens. + assert_lamports( + &mut banks_client, + &hyperlane_token_accounts.native_collateral, + native_collateral_account_lamports_before + transfer_amount, + ) + .await; + + // And let's take a look at the dispatched message account data to verify the message looks right. + let dispatched_message_account_data = banks_client + .get_account(dispatched_message_key) + .await + .unwrap() + .unwrap() + .data; + let dispatched_message = + DispatchedMessageAccount::fetch(&mut &dispatched_message_account_data[..]) + .unwrap() + .into_inner(); + + let transfer_remote_tx_status = banks_client + .get_transaction_status(tx_signature) + .await + .unwrap() + .unwrap(); + + let message = HyperlaneMessage { + version: 3, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: program_id.to_bytes().into(), + destination: REMOTE_DOMAIN, + recipient: remote_router, + // Expect the remote_transfer_amount to be in the message. + body: TokenMessage::new(remote_token_recipient, remote_transfer_amount, test_memo).to_vec(), + }; + + assert_eq!( + dispatched_message, + Box::new(DispatchedMessage::new( + message.nonce, + transfer_remote_tx_status.slot, + unique_message_account_keypair.pubkey(), + message.to_vec(), + )), + ); + + // And let's also look at the gas payment account to verify the gas payment looks right. + let gas_payment_account_data = banks_client + .get_account(gas_payment_pda_key) + .await + .unwrap() + .unwrap() + .data; + let gas_payment = GasPaymentAccount::fetch(&mut &gas_payment_account_data[..]) + .unwrap() + .into_inner(); + + assert_eq!( + *gas_payment, + GasPaymentData { + sequence_number: 0, + igp: igp_accounts.igp, + destination_domain: REMOTE_DOMAIN, + message_id: message.id(), + gas_amount: REMOTE_GAS_AMOUNT, + unique_gas_payment_pubkey: unique_message_account_keypair.pubkey(), + slot: transfer_remote_tx_status.slot, + payment: REMOTE_GAS_AMOUNT + } + .into(), + ); +} + +async fn transfer_from_remote( + initial_native_collateral_balance: u64, + remote_transfer_amount: U256, + sender_override: Option, + origin_override: Option, +) -> Result<(BanksClient, HyperlaneTokenAccounts, Pubkey), BanksClientError> { + let program_id = hyperlane_sealevel_token_native_id(); + let mailbox_program_id = mailbox_id(); + + let (mut banks_client, payer) = setup_client().await; + + let mailbox_accounts = initialize_mailbox( + &mut banks_client, + &mailbox_program_id, + &payer, + LOCAL_DOMAIN, + ONE_SOL_IN_LAMPORTS, + ProtocolFee::default(), + ) + .await + .unwrap(); + + let igp_accounts = + initialize_igp_accounts(&mut banks_client, &igp_program_id(), &payer, REMOTE_DOMAIN) + .await + .unwrap(); + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, Some(&igp_accounts)) + .await + .unwrap(); + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + // The native collateral account will have some lamports because it's rent-exempt. + let current_native_collateral_balance = banks_client + .get_balance(hyperlane_token_accounts.native_collateral) + .await + .unwrap(); + + // Give an initial balance to the native collateral account which will be used by the + // transfer_from_remote. + transfer_lamports( + &mut banks_client, + &payer, + &hyperlane_token_accounts.native_collateral, + initial_native_collateral_balance.saturating_sub(current_native_collateral_balance), + ) + .await; + + let recipient_pubkey = Pubkey::new_unique(); + let recipient: H256 = recipient_pubkey.to_bytes().into(); + + let message = HyperlaneMessage { + version: 3, + nonce: 0, + origin: origin_override.unwrap_or(REMOTE_DOMAIN), + // Default to the remote router as the sender + sender: sender_override.unwrap_or(remote_router), + destination: LOCAL_DOMAIN, + recipient: program_id.to_bytes().into(), + body: TokenMessage::new(recipient, remote_transfer_amount, vec![]).to_vec(), + }; + + process( + &mut banks_client, + &payer, + &mailbox_accounts, + vec![], + &message, + ) + .await?; + + Ok((banks_client, hyperlane_token_accounts, recipient_pubkey)) +} + +// Tests when the SPL token is the non-2022 version +#[tokio::test] +async fn test_transfer_from_success() { + let initial_native_collateral_balance = 100 * 10u64.pow(LOCAL_DECIMALS_U32); + let local_transfer_amount = 69 * 10u64.pow(LOCAL_DECIMALS_U32); + let remote_transfer_amount = convert_decimals( + local_transfer_amount.into(), + LOCAL_DECIMALS, + REMOTE_DECIMALS, + ) + .unwrap(); + + let (mut banks_client, hyperlane_token_accounts, recipient_associated_token_account) = + transfer_from_remote( + initial_native_collateral_balance, + remote_transfer_amount, + None, + None, + ) + .await + .unwrap(); + + // Check that the recipient's ATA got the tokens! + assert_lamports( + &mut banks_client, + &recipient_associated_token_account, + local_transfer_amount, + ) + .await; + + // And that the native collateral's balance is lower because it was spent in the transfer. + assert_lamports( + &mut banks_client, + &hyperlane_token_accounts.native_collateral, + initial_native_collateral_balance - local_transfer_amount, + ) + .await; +} + +#[tokio::test] +async fn test_transfer_from_remote_errors_if_sender_not_router() { + let initial_native_collateral_balance = 100 * 10u64.pow(LOCAL_DECIMALS_U32); + let local_transfer_amount = 69 * 10u64.pow(LOCAL_DECIMALS_U32); + let remote_transfer_amount = convert_decimals( + local_transfer_amount.into(), + LOCAL_DECIMALS, + REMOTE_DECIMALS, + ) + .unwrap(); + + // Same remote domain origin, but wrong sender. + let result = transfer_from_remote( + initial_native_collateral_balance, + remote_transfer_amount, + Some(H256::random()), + None, + ) + .await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidInstructionData), + ); + + // Wrong remote domain origin, but correct sender. + let result = transfer_from_remote( + initial_native_collateral_balance, + remote_transfer_amount, + None, + Some(REMOTE_DOMAIN + 1), + ) + .await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidInstructionData), + ); +} + +#[tokio::test] +async fn test_transfer_from_remote_errors_if_process_authority_not_signer() { + let program_id = hyperlane_sealevel_token_native_id(); + let mailbox_program_id = mailbox_id(); + + let (mut banks_client, payer) = setup_client().await; + + let _mailbox_accounts = initialize_mailbox( + &mut banks_client, + &mailbox_program_id, + &payer, + LOCAL_DOMAIN, + ONE_SOL_IN_LAMPORTS, + ProtocolFee::default(), + ) + .await + .unwrap(); + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + let recipient_pubkey = Pubkey::new_unique(); + let recipient: H256 = recipient_pubkey.to_bytes().into(); + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Try calling directly into the message handler, skipping the mailbox. + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &MessageRecipientInstruction::Handle(HandleInstruction { + origin: REMOTE_DOMAIN, + sender: remote_router, + message: TokenMessage::new(recipient, 12345u64.into(), vec![]).to_vec(), + }) + .encode() + .unwrap(), + vec![ + // Recipient.handle accounts + // 0. `[signer]` Mailbox processor authority specific to this program. + // 1. `[executable]` system_program + // 2. `[]` hyperlane_token storage + // 3. `[writeable]` recipient wallet address + // 4. `[executable]` The system program. + // 5. `[writeable]` The native token collateral PDA account. + AccountMeta::new_readonly( + hyperlane_token_accounts.mailbox_process_authority, + false, + ), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new_readonly(hyperlane_token_accounts.token, false), + AccountMeta::new(recipient_pubkey, false), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(hyperlane_token_accounts.native_collateral, false), + ], + )], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature), + ); +} + +#[tokio::test] +async fn test_enroll_remote_router() { + let program_id = hyperlane_sealevel_token_native_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + // Verify the remote router was enrolled. + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!( + token.remote_routers, + vec![(REMOTE_DOMAIN, remote_router)].into_iter().collect(), + ); +} + +#[tokio::test] +async fn test_enroll_remote_router_errors_if_not_signed_by_owner() { + let program_id = hyperlane_sealevel_token_native_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + let non_owner = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + + // Use the mint authority as the payer, which has a balance but is not the owner, + // so we expect this to fail. + let result = enroll_remote_router( + &mut banks_client, + &program_id, + &non_owner, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + H256::random(), + ) + .await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); + + // Also try using the mint authority as the payer and specifying the correct + // owner account, but the owner isn't a signer: + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Enroll the remote router + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::EnrollRemoteRouter(RemoteRouterConfig { + domain: REMOTE_DOMAIN, + router: Some(H256::random()), + }) + .encode() + .unwrap(), + vec![ + AccountMeta::new(solana_program::system_program::id(), false), + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), false), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature), + ); +} + +#[tokio::test] +async fn test_set_destination_gas_configs() { + let program_id = hyperlane_sealevel_token_native_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + // Set the destination gas config + let gas = 111222333; + set_destination_gas_config( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + gas, + ) + .await + .unwrap(); + + // Verify the destination gas was set. + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!( + token.destination_gas, + vec![(REMOTE_DOMAIN, gas)].into_iter().collect(), + ); +} + +#[tokio::test] +async fn test_set_destination_gas_configs_errors_if_not_signed_by_owner() { + let program_id = hyperlane_sealevel_token_native_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + let non_owner = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + + // Use the non_owner as the payer, which has a balance but is not the owner, + // so we expect this to fail. + let gas = 111222333; + let result = set_destination_gas_config( + &mut banks_client, + &program_id, + &non_owner, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + gas, + ) + .await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); + + // Also try using the non_owner as the payer and specifying the correct + // owner account, but the owner isn't a signer: + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Try setting + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetDestinationGasConfigs(vec![GasRouterConfig { + domain: REMOTE_DOMAIN, + gas: Some(gas), + }]) + .encode() + .unwrap(), + vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), false), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature), + ); +} + +#[tokio::test] +async fn test_transfer_ownership() { + let program_id = hyperlane_sealevel_token_native_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + let new_owner = Some(Pubkey::new_unique()); + + // Transfer ownership + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::TransferOwnership(new_owner) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + // Verify the new owner is set + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!(token.owner, new_owner); +} + +#[tokio::test] +async fn test_transfer_ownership_errors_if_owner_not_signer() { + let program_id = hyperlane_sealevel_token_native_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + let new_owner = Some(Pubkey::new_unique()); + let non_owner = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + + // Try transferring ownership using the mint authority key + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::TransferOwnership(new_owner) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(non_owner.pubkey(), true), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); +} + +#[tokio::test] +async fn test_set_interchain_security_module() { + let program_id = hyperlane_sealevel_token_native_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + let new_ism = Some(Pubkey::new_unique()); + + // Set the ISM + // Transfer ownership + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainSecurityModule(new_ism) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + // Verify the new ISM is set + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!(token.interchain_security_module, new_ism); +} + +#[tokio::test] +async fn test_set_interchain_security_module_errors_if_owner_not_signer() { + let program_id = hyperlane_sealevel_token_native_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + let new_ism = Some(Pubkey::new_unique()); + let non_owner = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + + // Try setting the ISM using the non_owner key + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainSecurityModule(new_ism) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(non_owner.pubkey(), true), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); + + // Also try using the non_owner as the payer and specifying the correct + // owner account, but the owner isn't a signer: + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainSecurityModule(new_ism) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), false), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature), + ); +} + +#[tokio::test] +async fn test_set_interchain_gas_paymaster() { + let program_id = hyperlane_sealevel_token_native_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + let new_igp = Some(( + Pubkey::new_unique(), + InterchainGasPaymasterType::OverheadIgp(Pubkey::new_unique()), + )); + + // Set the IGP + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainGasPaymaster(new_igp.clone()) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + // Verify the new IGP is set + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!(token.interchain_gas_paymaster, new_igp); +} + +#[tokio::test] +async fn test_set_interchain_gas_paymaster_errors_if_owner_not_signer() { + let program_id = hyperlane_sealevel_token_native_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None) + .await + .unwrap(); + + let new_igp = Some(( + Pubkey::new_unique(), + InterchainGasPaymasterType::OverheadIgp(Pubkey::new_unique()), + )); + let non_owner = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + + // Try setting the ISM using the mint authority key + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainGasPaymaster(new_igp.clone()) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(non_owner.pubkey(), true), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); + + // Also try using the non_owner as the payer and specifying the correct + // owner account, but the owner isn't a signer: + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainGasPaymaster(new_igp) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), false), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature), + ); +} diff --git a/solidity/contracts/token/HypNative.sol b/solidity/contracts/token/HypNative.sol index 8a55d9b0d9a..99ae599d742 100644 --- a/solidity/contracts/token/HypNative.sol +++ b/solidity/contracts/token/HypNative.sol @@ -46,7 +46,7 @@ contract HypNative is FungibleTokenRouter { uint32 _destination, bytes32 _recipient, uint256 _amount - ) external payable virtual override returns (bytes32 messageId) { + ) public payable virtual override returns (bytes32 messageId) { require(msg.value >= _amount, "Native: amount exceeds msg.value"); uint256 _hookPayment = msg.value - _amount; return _transferRemote(_destination, _recipient, _amount, _hookPayment); @@ -89,7 +89,7 @@ contract HypNative is FungibleTokenRouter { */ function _transferFromSender( uint256 - ) internal pure override returns (bytes memory) { + ) internal virtual override returns (bytes memory) { return bytes(""); // no metadata } diff --git a/solidity/contracts/token/extensions/HypERC20CollateralMemo.sol b/solidity/contracts/token/extensions/HypERC20CollateralMemo.sol new file mode 100644 index 00000000000..3c2c50b8b53 --- /dev/null +++ b/solidity/contracts/token/extensions/HypERC20CollateralMemo.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.0; + +import {HypERC20Collateral} from "../HypERC20Collateral.sol"; + +// collateral +contract HypERC20CollateralMemo is HypERC20Collateral { + event IncludedMemo(bytes memo); + bytes private _memo; + + constructor( + address erc20, + uint256 _scale, + address _mailbox + ) HypERC20Collateral(erc20, _scale, _mailbox) {} + + function transferRemoteMemo( + uint32 _destination, + bytes32 _recipient, + uint256 _amountOrId, + bytes calldata memo + ) external payable virtual returns (bytes32 messageId) { + _memo = memo; + return + _transferRemote(_destination, _recipient, _amountOrId, msg.value); + } + + function _transferFromSender( + uint256 _amount + ) internal virtual override returns (bytes memory) { + super._transferFromSender(_amount); + bytes memory memo = _memo; + delete _memo; + emit IncludedMemo(memo); + return memo; + } +} diff --git a/solidity/contracts/token/extensions/HypERC20Memo.sol b/solidity/contracts/token/extensions/HypERC20Memo.sol new file mode 100644 index 00000000000..2c27e76e7d1 --- /dev/null +++ b/solidity/contracts/token/extensions/HypERC20Memo.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.0; + +import {HypERC20} from "../HypERC20.sol"; + +// synthetic +contract HypERC20Memo is HypERC20 { + event IncludedMemo(bytes memo); + bytes private _memo; + + constructor( + uint8 __decimals, + uint256 _scale, + address _mailbox + ) HypERC20(__decimals, _scale, _mailbox) {} + + function transferRemoteMemo( + uint32 _destination, + bytes32 _recipient, + uint256 _amountOrId, + bytes calldata memo + ) external payable virtual returns (bytes32 messageId) { + _memo = memo; + return + _transferRemote(_destination, _recipient, _amountOrId, msg.value); + } + + function _transferFromSender( + uint256 _amount + ) internal virtual override returns (bytes memory) { + super._transferFromSender(_amount); + bytes memory memo = _memo; + delete _memo; + emit IncludedMemo(memo); + return memo; + } +} diff --git a/solidity/contracts/token/extensions/HypNativeMemo.sol b/solidity/contracts/token/extensions/HypNativeMemo.sol new file mode 100644 index 00000000000..dccf42cd19d --- /dev/null +++ b/solidity/contracts/token/extensions/HypNativeMemo.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.0; + +import {HypNative} from "../HypNative.sol"; + +// native +contract HypNativeMemo is HypNative { + event IncludedMemo(bytes memo); + bytes private _memo; + + constructor(uint256 _scale, address _mailbox) HypNative(_scale, _mailbox) {} + + function transferRemoteMemo( + uint32 _destination, + bytes32 _recipient, + uint256 _amount, + bytes calldata memo + ) external payable virtual returns (bytes32 messageId) { + _memo = memo; + return super.transferRemote(_destination, _recipient, _amount); + } + + function _transferFromSender( + uint256 _amount + ) internal override returns (bytes memory) { + // no super call, parent + super._transferFromSender(_amount); + bytes memory memo = _memo; + delete _memo; + emit IncludedMemo(memo); + return memo; + } +} diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index 91942b0e5fa..eef49177d01 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -34,11 +34,17 @@ import { createAdvancedIsmConfig } from './ism.js'; const TYPE_DESCRIPTIONS: Record = { [TokenType.synthetic]: 'A new ERC20 with remote transfer functionality', + [TokenType.syntheticMemo]: + 'A new ERC20 with remote transfer functionality, with outbound memo support', [TokenType.syntheticRebase]: `A rebasing ERC20 with remote transfer functionality. Must be paired with ${TokenType.collateralVaultRebase}`, [TokenType.collateral]: 'Extends an existing ERC20 with remote transfer functionality', + [TokenType.collateralMemo]: + 'Extends an existing ERC20 with remote transfer functionality, with outbound memo support', [TokenType.native]: 'Extends the native token with remote transfer functionality', + [TokenType.nativeMemo]: + 'Extends the native token with remote transfer functionality, with outbound memo support', [TokenType.collateralVault]: 'Extends an existing ERC4626 with remote transfer functionality. Yields are manually claimed by owner.', [TokenType.collateralVaultRebase]: @@ -168,6 +174,7 @@ export async function createWarpRouteDeployConfig({ switch (type) { case TokenType.collateral: + case TokenType.collateralMemo: case TokenType.XERC20: case TokenType.XERC20Lockbox: case TokenType.collateralFiat: diff --git a/typescript/sdk/src/token/EvmERC20WarpRouteReader.hardhat-test.ts b/typescript/sdk/src/token/EvmERC20WarpRouteReader.hardhat-test.ts index 48c6364c36a..9a6e28cb229 100644 --- a/typescript/sdk/src/token/EvmERC20WarpRouteReader.hardhat-test.ts +++ b/typescript/sdk/src/token/EvmERC20WarpRouteReader.hardhat-test.ts @@ -91,9 +91,12 @@ describe('ERC20WarpRouterReader', async () => { it('should derive a token type from contract', async () => { const typesToDerive = [ TokenType.collateral, + TokenType.collateralMemo, TokenType.collateralVault, TokenType.synthetic, + TokenType.syntheticMemo, TokenType.native, + TokenType.nativeMemo, ] as const; await Promise.all( diff --git a/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts index 1d31f4c3e71..f0040881db1 100644 --- a/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts +++ b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts @@ -1,7 +1,9 @@ import { BigNumber, Contract, constants } from 'ethers'; import { + HypERC20CollateralMemo__factory, HypERC20Collateral__factory, + HypERC20Memo__factory, HypERC20__factory, HypERC4626Collateral__factory, HypERC4626OwnerCollateral__factory, @@ -118,6 +120,10 @@ export class EvmERC20WarpRouteReader extends HyperlaneReader { factory: HypERC20Collateral__factory, method: 'wrappedToken', }, + [TokenType.collateralMemo]: { + factory: HypERC20CollateralMemo__factory, + method: 'wrappedToken', + }, [TokenType.syntheticRebase]: { factory: HypERC4626__factory, method: 'collateralDomain', @@ -126,6 +132,10 @@ export class EvmERC20WarpRouteReader extends HyperlaneReader { factory: HypERC20__factory, method: 'decimals', }, + [TokenType.syntheticMemo]: { + factory: HypERC20Memo__factory, + method: 'decimals', + }, }; // Temporarily turn off SmartProvider logging diff --git a/typescript/sdk/src/token/Token.test.ts b/typescript/sdk/src/token/Token.test.ts index d4bd4daef26..2f261e8f2df 100644 --- a/typescript/sdk/src/token/Token.test.ts +++ b/typescript/sdk/src/token/Token.test.ts @@ -39,6 +39,14 @@ const STANDARD_TO_TOKEN: Record = { symbol: 'INJ', name: 'Injective Coin', }, + [TokenStandard.EvmHypNativeMemo]: { + chainName: TestChainName.test2, + standard: TokenStandard.EvmHypNativeMemo, + addressOrDenom: '0x26f32245fCF5Ad53159E875d5Cae62aEcf19c2d4', // TODO: check + decimals: 18, + symbol: 'INJ', + name: 'Injective Coin', + }, [TokenStandard.EvmHypCollateral]: { chainName: TestChainName.test3, standard: TokenStandard.EvmHypCollateral, @@ -48,6 +56,15 @@ const STANDARD_TO_TOKEN: Record = { symbol: 'USDC', name: 'USDC', }, + [TokenStandard.EvmHypCollateralMemo]: { + chainName: TestChainName.test3, + standard: TokenStandard.EvmHypCollateralMemo, + addressOrDenom: '0x31b5234A896FbC4b3e2F7237592D054716762131', // TODO: check + collateralAddressOrDenom: '0x64544969ed7ebf5f083679233325356ebe738930', // TODO: check + decimals: 18, + symbol: 'USDC', + name: 'USDC', + }, [TokenStandard.EvmHypRebaseCollateral]: { chainName: TestChainName.test3, standard: TokenStandard.EvmHypRebaseCollateral, @@ -78,6 +95,14 @@ const STANDARD_TO_TOKEN: Record = { [TokenStandard.EvmHypSynthetic]: { chainName: TestChainName.test2, standard: TokenStandard.EvmHypSynthetic, + addressOrDenom: '0x8358D8291e3bEDb04804975eEa0fe9fe0fAfB147', // TODO: check + decimals: 6, + symbol: 'USDC', + name: 'USDC', + }, + [TokenStandard.EvmHypSyntheticMemo]: { + chainName: TestChainName.test2, + standard: TokenStandard.EvmHypSyntheticMemo, addressOrDenom: '0x8358D8291e3bEDb04804975eEa0fe9fe0fAfB147', decimals: 6, symbol: 'USDC', diff --git a/typescript/sdk/src/token/Token.ts b/typescript/sdk/src/token/Token.ts index a09be1b85aa..9cdff197fd7 100644 --- a/typescript/sdk/src/token/Token.ts +++ b/typescript/sdk/src/token/Token.ts @@ -190,12 +190,16 @@ export class Token implements IToken { `Token chain ${chainName} not found in multiProvider`, ); - if (standard === TokenStandard.EvmHypNative) { + if ( + standard === TokenStandard.EvmHypNative || + standard === TokenStandard.EvmHypNativeMemo + ) { return new EvmHypNativeAdapter(chainName, multiProvider, { token: addressOrDenom, }); } else if ( standard === TokenStandard.EvmHypCollateral || + standard === TokenStandard.EvmHypCollateralMemo || standard === TokenStandard.EvmHypOwnerCollateral ) { return new EvmHypCollateralAdapter(chainName, multiProvider, { @@ -209,7 +213,10 @@ export class Token implements IToken { return new EvmHypCollateralFiatAdapter(chainName, multiProvider, { token: addressOrDenom, }); - } else if (standard === TokenStandard.EvmHypSynthetic) { + } else if ( + standard === TokenStandard.EvmHypSynthetic || + standard === TokenStandard.EvmHypSyntheticMemo + ) { return new EvmHypSyntheticAdapter(chainName, multiProvider, { token: addressOrDenom, }); diff --git a/typescript/sdk/src/token/TokenStandard.ts b/typescript/sdk/src/token/TokenStandard.ts index 536ba85e1f5..8082097feb8 100644 --- a/typescript/sdk/src/token/TokenStandard.ts +++ b/typescript/sdk/src/token/TokenStandard.ts @@ -13,11 +13,14 @@ export enum TokenStandard { ERC721 = 'ERC721', EvmNative = 'EvmNative', EvmHypNative = 'EvmHypNative', + EvmHypNativeMemo = 'EvmHypNativeMemo', EvmHypCollateral = 'EvmHypCollateral', + EvmHypCollateralMemo = 'EvmHypCollateralMemo', EvmHypOwnerCollateral = 'EvmHypOwnerCollateral', EvmHypRebaseCollateral = 'EvmHypRebaseCollateral', EvmHypCollateralFiat = 'EvmHypCollateralFiat', EvmHypSynthetic = 'EvmHypSynthetic', + EvmHypSyntheticMemo = 'EvmHypSyntheticMemo', EvmHypSyntheticRebase = 'EvmHypSyntheticRebase', EvmHypXERC20 = 'EvmHypXERC20', EvmHypXERC20Lockbox = 'EvmHypXERC20Lockbox', @@ -63,11 +66,14 @@ export const TOKEN_STANDARD_TO_PROTOCOL: Record = { ERC721: ProtocolType.Ethereum, EvmNative: ProtocolType.Ethereum, EvmHypNative: ProtocolType.Ethereum, + EvmHypNativeMemo: ProtocolType.Ethereum, EvmHypCollateral: ProtocolType.Ethereum, + EvmHypCollateralMemo: ProtocolType.Ethereum, EvmHypOwnerCollateral: ProtocolType.Ethereum, EvmHypRebaseCollateral: ProtocolType.Ethereum, EvmHypCollateralFiat: ProtocolType.Ethereum, EvmHypSynthetic: ProtocolType.Ethereum, + EvmHypSyntheticMemo: ProtocolType.Ethereum, EvmHypSyntheticRebase: ProtocolType.Ethereum, EvmHypXERC20: ProtocolType.Ethereum, EvmHypXERC20Lockbox: ProtocolType.Ethereum, @@ -129,7 +135,9 @@ export const TOKEN_NFT_STANDARDS = [ export const TOKEN_COLLATERALIZED_STANDARDS = [ TokenStandard.EvmHypCollateral, + TokenStandard.EvmHypCollateralMemo, TokenStandard.EvmHypNative, + TokenStandard.EvmHypNativeMemo, TokenStandard.SealevelHypCollateral, TokenStandard.SealevelHypNative, TokenStandard.CwHypCollateral, @@ -155,11 +163,14 @@ export const MINT_LIMITED_STANDARDS = [ export const TOKEN_HYP_STANDARDS = [ TokenStandard.EvmHypNative, + TokenStandard.EvmHypNativeMemo, TokenStandard.EvmHypCollateral, + TokenStandard.EvmHypCollateralMemo, TokenStandard.EvmHypCollateralFiat, TokenStandard.EvmHypOwnerCollateral, TokenStandard.EvmHypRebaseCollateral, TokenStandard.EvmHypSynthetic, + TokenStandard.EvmHypSyntheticMemo, TokenStandard.EvmHypSyntheticRebase, TokenStandard.EvmHypXERC20, TokenStandard.EvmHypXERC20Lockbox, @@ -196,7 +207,9 @@ export const TOKEN_COSMWASM_STANDARDS = [ export const TOKEN_TYPE_TO_STANDARD: Record = { [TokenType.native]: TokenStandard.EvmHypNative, + [TokenType.nativeMemo]: TokenStandard.EvmHypNativeMemo, [TokenType.collateral]: TokenStandard.EvmHypCollateral, + [TokenType.collateralMemo]: TokenStandard.EvmHypCollateralMemo, [TokenType.collateralFiat]: TokenStandard.EvmHypCollateralFiat, [TokenType.XERC20]: TokenStandard.EvmHypXERC20, [TokenType.XERC20Lockbox]: TokenStandard.EvmHypXERC20Lockbox, @@ -204,6 +217,7 @@ export const TOKEN_TYPE_TO_STANDARD: Record = { [TokenType.collateralVaultRebase]: TokenStandard.EvmHypRebaseCollateral, [TokenType.collateralUri]: TokenStandard.EvmHypCollateral, [TokenType.synthetic]: TokenStandard.EvmHypSynthetic, + [TokenType.syntheticMemo]: TokenStandard.EvmHypSyntheticMemo, [TokenType.syntheticRebase]: TokenStandard.EvmHypSyntheticRebase, [TokenType.syntheticUri]: TokenStandard.EvmHypSynthetic, [TokenType.nativeScaled]: TokenStandard.EvmHypNative, diff --git a/typescript/sdk/src/token/config.ts b/typescript/sdk/src/token/config.ts index 9ba18df6174..275cd3209bf 100644 --- a/typescript/sdk/src/token/config.ts +++ b/typescript/sdk/src/token/config.ts @@ -1,8 +1,10 @@ export enum TokenType { synthetic = 'synthetic', + syntheticMemo = 'syntheticMemo', syntheticRebase = 'syntheticRebase', syntheticUri = 'syntheticUri', collateral = 'collateral', + collateralMemo = 'collateralMemo', collateralVault = 'collateralVault', collateralVaultRebase = 'collateralVaultRebase', XERC20 = 'xERC20', @@ -10,6 +12,7 @@ export enum TokenType { collateralFiat = 'collateralFiat', collateralUri = 'collateralUri', native = 'native', + nativeMemo = 'nativeMemo', // backwards compatible alias to native nativeScaled = 'nativeScaled', } @@ -17,8 +20,10 @@ export enum TokenType { export const gasOverhead = (tokenType: TokenType): number => { switch (tokenType) { case TokenType.synthetic: + case TokenType.syntheticMemo: return 64_000; case TokenType.native: + case TokenType.nativeMemo: return 44_000; default: return 68_000; diff --git a/typescript/sdk/src/token/contracts.ts b/typescript/sdk/src/token/contracts.ts index a50740e66d2..d6a206938aa 100644 --- a/typescript/sdk/src/token/contracts.ts +++ b/typescript/sdk/src/token/contracts.ts @@ -1,5 +1,7 @@ import { + HypERC20CollateralMemo__factory, HypERC20Collateral__factory, + HypERC20Memo__factory, HypERC20__factory, HypERC721Collateral__factory, HypERC721URICollateral__factory, @@ -9,6 +11,7 @@ import { HypERC4626OwnerCollateral__factory, HypERC4626__factory, HypFiatToken__factory, + HypNativeMemo__factory, HypNative__factory, HypXERC20Lockbox__factory, HypXERC20__factory, @@ -18,14 +21,17 @@ import { TokenType } from './config.js'; export const hypERC20contracts = { [TokenType.synthetic]: 'HypERC20', + [TokenType.syntheticMemo]: 'HypERC20Memo', [TokenType.syntheticRebase]: 'HypERC4626', [TokenType.collateral]: 'HypERC20Collateral', + [TokenType.collateralMemo]: 'HypERC20CollateralMemo', [TokenType.collateralFiat]: 'HypFiatToken', [TokenType.XERC20]: 'HypXERC20', [TokenType.XERC20Lockbox]: 'HypXERC20Lockbox', [TokenType.collateralVault]: 'HypERC4626OwnerCollateral', [TokenType.collateralVaultRebase]: 'HypERC4626Collateral', [TokenType.native]: 'HypNative', + [TokenType.nativeMemo]: 'HypNativeMemo', // uses same contract as native [TokenType.nativeScaled]: 'HypNative', }; @@ -33,7 +39,9 @@ export type HypERC20contracts = typeof hypERC20contracts; export const hypERC20factories = { [TokenType.synthetic]: new HypERC20__factory(), + [TokenType.syntheticMemo]: new HypERC20Memo__factory(), [TokenType.collateral]: new HypERC20Collateral__factory(), + [TokenType.collateralMemo]: new HypERC20CollateralMemo__factory(), [TokenType.collateralVault]: new HypERC4626OwnerCollateral__factory(), [TokenType.collateralVaultRebase]: new HypERC4626Collateral__factory(), [TokenType.syntheticRebase]: new HypERC4626__factory(), @@ -41,6 +49,7 @@ export const hypERC20factories = { [TokenType.XERC20]: new HypXERC20__factory(), [TokenType.XERC20Lockbox]: new HypXERC20Lockbox__factory(), [TokenType.native]: new HypNative__factory(), + [TokenType.nativeMemo]: new HypNativeMemo__factory(), [TokenType.nativeScaled]: new HypNative__factory(), }; export type HypERC20Factories = typeof hypERC20factories; diff --git a/typescript/sdk/src/token/types.test.ts b/typescript/sdk/src/token/types.test.ts index cec6aedb2b1..9c060523549 100644 --- a/typescript/sdk/src/token/types.test.ts +++ b/typescript/sdk/src/token/types.test.ts @@ -14,11 +14,16 @@ import { const SOME_ADDRESS = ethers.Wallet.createRandom().address; const COLLATERAL_TYPES = [ TokenType.collateral, + TokenType.collateralMemo, TokenType.collateralUri, TokenType.collateralVault, ]; -const NON_COLLATERAL_TYPES = [TokenType.synthetic, TokenType.syntheticUri]; +const NON_COLLATERAL_TYPES = [ + TokenType.synthetic, + TokenType.syntheticMemo, + TokenType.syntheticUri, +]; describe('WarpRouteDeployConfigSchema refine', () => { let config: WarpRouteDeployConfig; diff --git a/typescript/sdk/src/token/types.ts b/typescript/sdk/src/token/types.ts index e8c89dd321b..ccd1cce19ff 100644 --- a/typescript/sdk/src/token/types.ts +++ b/typescript/sdk/src/token/types.ts @@ -28,7 +28,11 @@ export type TokenMetadata = z.infer; export const isTokenMetadata = isCompliant(TokenMetadataSchema); export const NativeTokenConfigSchema = TokenMetadataSchema.partial().extend({ - type: z.enum([TokenType.native, TokenType.nativeScaled]), + type: z.enum([ + TokenType.native, + TokenType.nativeMemo, + TokenType.nativeScaled, + ]), }); export type NativeTokenConfig = z.infer; export const isNativeTokenConfig = isCompliant(NativeTokenConfigSchema); @@ -37,6 +41,7 @@ export const CollateralTokenConfigSchema = TokenMetadataSchema.partial().extend( { type: z.enum([ TokenType.collateral, + TokenType.collateralMemo, TokenType.collateralVault, TokenType.collateralVaultRebase, TokenType.collateralFiat, @@ -95,7 +100,11 @@ export const isCollateralRebaseTokenConfig = isCompliant( ); export const SyntheticTokenConfigSchema = TokenMetadataSchema.partial().extend({ - type: z.enum([TokenType.synthetic, TokenType.syntheticUri]), + type: z.enum([ + TokenType.synthetic, + TokenType.syntheticMemo, + TokenType.syntheticUri, + ]), initialSupply: z.string().or(z.number()).optional(), }); export type SyntheticTokenConfig = z.infer;