Secure validation for TON blockchain via cold wallet - a simple firewall contract that is easy to review.
This is an alternative simplified implementation for the Nominator Pool smart contract, that supports a single nominator only. The benefit of this implementation is that it's more secure since the attack surface is considerably smaller. This is due to massive reduction in complexity of Nominator Pool that has to support multiple third-party nominators.
This smart contract is intended to be the go-to solution for TON validators that have enough stake to validate by themselves. The other available alternatives are:
- using a hot wallet (insecure since a cold wallet is needed to prevent theft if the validator node is hacked)
- using restricted-wallet (which is unmaintained and has unresolved attack vectors like gas drainage attacks)
- using Nominator Pool with max_nominators_count = 1 (unnecessarily complex with a larger attack surface)
See a more detailed comparison of existing alternatives below.
The architecture is nearly identical to the Nominator Pool contract:
- Owner - cold wallet (private key that is not connected to the Internet) that owns the funds used for staking and acts as the single nominator
- Validator - the wallet whose private key is on the validator node (can sign blocks but can't steal the funds used for stake)
- Owner holds the funds for staking ($$$) in their secure cold wallet
- Owner deposits the funds ($$$) in the SingleNominator contract (this contract)
- MyTonCtrl starts running on the validator node connected to the Internet
- MyTonCtrl uses Validator wallet to instruct SingleNominator to enter the next election cycle
- SingleNominator sends the stake ($$$) to the Elector for one cycle
- The election cycle is over and stake can be recovered
- MyTonCtrl uses Validator wallet to instruct SingleNominator to recover the stake from the election cycle
- SingleNominator recovers the stake ($$$) of the previous cycle from the Elector
- Steps 4-8 repeat as long as Owner is happy to keep validating
- Owner withdraws the funds ($$$) from the SingleNominator contract and takes them back home
-
The validator node requires a hot wallet to sign new blocks. This wallet is inherently insecure because its private key is connected to the Internet. Even if this key is compromised, the Validator cannot extract the funds used for validation. Only Owner can withdraw these funds.
-
Even if Validator wallet is compromised, Owner can tell SingleNominator to change the validator address. This will prevent the attacker from interacting with SingleNominator further. There is no race condition here, Owner will always take precedence.
-
SingleNominator balance holds the principal staking funds only - its balance is not used for gas fees. Gas money for entering election cycles is held in the Validator wallet. This prevents an attacker that compromised the validator from draining the principal via a gas spending attack.
-
SingleNominator verifies the format of all operations given by Validator to make sure it doesn't forward invalid messages to the Elector.
-
On emergency, for example if Elector contract was upgraded and changes its interface, Owner can still send any raw message as SingleNominator to recover the stake from Elector.
-
On extreme emergency, Owner can set the code of SingleNominator and override its current logic to address unforeseen circumstances.
Some of these attack vectors cannot be mitigated using the regular Nominator Pool contract because that would allow the person running the validator to steal funds from its nominators. This is not a problem with SingleNominator because Owner and Validator are owned by the same party.
Coming soon - feel free to report any problem in issues
- Review the contract and tests to make sure you're happy with the implementation
- Install the project using
npm install
(make sure you have all dependencies from tonstarter) - Build the contract using
npm run build
- Run the tests using
npm run test
- Deploy the contract using
npm run deploy
- The contract plugs in seamlessly to MyTonCtrl with the same interface as Nominator Pool
- A single instance of the contract is used for both even and odd validation cycles (single contract config in MyTonCtrl). The stake amounts configured in MyTonCtrl must be absolute (set to half the total stake amount) to support the single instance mode.
Assuming that you are a validator with enough stake to validate by yourself, these are the alternative setups you can use with MyTonCtrl:
This is the simplest setup where MyTonCtrl is connected to the same standard wallet that holds the funds. Since this wallet is connected to the Internet, it is considered a hot wallet.
This is insecure since an attacker can get the private key as it's connected to the Internet. With the private key the attacker can send the staking funds to anyone.
This setup replaces the standard wallet with a restricted-wallet that allows outgoing transactions to be sent only to restricted destinations such as the Elector and the owner's address.
The restricted wallet is unmaintained (replaced by nominator-pool) and has unresolved attack vectors like gas drainage attacks. Since the same wallet holds both gas fees and the stake principal in the same balance, an attacker that compromises the private key can generate transactions that will cause significant principal losses. In addition, there's a race condition between the attacker and the owner when trying to withdraw due to seqno collisions.
The nominator-pool was the first to introduce clear separation between the owners of the stake (nominators) and the validator that is connected to the Internet. This setup supports up to 40 nominators staking together on the same validator.
The nominator pool contract is overly complex due to the support of 40 concurrent nominators. In addition, the contract has to protect the nominators from the contract deployer because those are separate entities. This setup is considered ok but is very difficult to audit in full due to the size of the attack surface. The solution makes sense mostly when the validator does not have enough stake to validate alone or wants to do rev-share with third-party stakeholders.
This is the setup implemented in this repo. It's a very simplified version of the nominator pool that supports a single nominator and does not need to protect this nominator from the contract deployer as they are the same entity.
If you have a single nominator that holds all stake for validation, this is the most secure setup you can use. On top of the simplicity, this contract provides the owner with multiple emergency safeguards that can recover stake even in extreme scenarios like Elector upgrades that break the recover stake interface.
In order to deploy the contract use the following procedure:
- set the following environment variables:
- OWNER_ADDRESS
- VALIDATOR_ADDRESS
- TON_ENDPOINT (default:
https://toncenter.com/api/v2/jsonRPC
) - TON_API_KEY
Environment variables can be set by export or using .env file. For example:export VALIDATOR_ADDRESS=Ef8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAU
or insertVALIDATOR_ADDRESS=Ef8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAU
to .env file.
- run
npm run init-deploy-wallet
to init deploy wallet. The script will deploy a wallet (if not already deployed) and print its address. Make sure you have funds in this address which will be used for deployment. 1 TON should be enough for deployment. The wallet mnemonics will be stored locally at ./build/deploy.config.json. This step should be run only once to init the deploy wallet. - run
npm run deploy
. This script will deploy the single-nominator contract (if not already deployed) with the OWNER_ADDRESS and VALIDATOR_ADDRESS which were set as the environment variables. - The deployment script will print the nominator contract address. Use str-to-addr.fif to create .addr file from the base64 string representation of the contract address.
This file will be used by Mytonctrl and should be placed on the validator node at~/.local/share/mytoncore/pools/
directory. Mytonctrl will search for pools in this folder (usePool should be set to true in Mytonctrl to use pools).
Example:fift -s scripts/fif/str-to-addr.fif Ef-C8SHoQ72S2fgqzhtUkzFG0krKKvIeCqpn4AjyXyhUUpIz
. - Before moving funds to the nominator contract it is important to approve the ownership of the owner address. It is recommended to send 1 TON to the nominator contract and use the withdrawal procedure described here.
Single nominator contract is compatible with Mytonctrl when set in usePool mode. The following steps should be taken:
- Copy the nominator .addr file (generated as described above) to
~/.local/share/mytoncore/pools/
. - From Mytonctrl use:
set usePool true
set stake 350000
to set the stake to 350,000. Make sure to change the stake to the desired amount.
- Make sure you have a validator wallet whose address match the VALIDATOR_ADDRESS that used when deploying the contract. You can use
scripts/ts/read-contract-state.ts
to read the owner and validator addresses. - Copy all fif script located in mytonctrl-scripts to the node under
~/.local/share/mytoncore/contracts/nominator-pool/func/
. Mytonctrl will need all the scripts in order for the validator node to operate smoothly.
The nominator owner has 4 roles.
Used to withdraw funds to the owner's wallet. To withdraw the funds the owner should send a message with a body that includes: opcode=0x1000 (32 bits), query_id (64 bits) and withdraw amount (stored as coin variable). The nominator contract will send the funds with BOUNCABLE flag and mode=64.
In case the owner is using a hot wallet (not recommended), withdraw-deeplink.ts can be used to generate a deeplink to initiate a withdrawal from tonkeeper wallet.
Command line: ts-node scripts/ts/withdraw-deeplink.ts single-nominator-addr withdraw-amount
where:
- single-nominator-addr is the single nominator address the owner wishes to withdraw from.
- withdraw-amount is the amount to withdraw. The nominator contract will leave 1 TON in the contract so the actual amount that will be sent to the owner address will be the minimum between the requested amount and the contract balance - 1.
The owner should run the deeplink from a phone with the tonkeeper wallet.
In case the owner is using a cold wallet (recommended), withdraw.fif can be used to generate a boc body which includes withdraw opcode and the amount to withdraw.
Command line: fift -s scripts/fif/withdraw.fif withdraw-amount
where withdraw-amount is the amount to withdraw from the nominator contract to the owner's wallet. As described above the nominator contract will leave at least 1 TON in the contract.
This script will generate a boc body (named withdraw.boc) that should be signed and send from the owner's wallet.
From the black computer the owner should run:
- create and sign the tx:
fift -s wallet-v3.fif my-wallet single_nominator_address sub_wallet_id seqno amount -B withdraw.boc
where my-wallet is the owner's pk file (without extension). For amount 1 TON should be enough to pay fees (remaining amount will be returned to owner). The withdraw.boc is the boc generated above. - from a computer with access to the internet run:
lite-client -C global.config.json -c 'sendfile wallet-query.boc'
to send the boc file (wallet-query.boc) generated in the prev step.
Used to change the validator address. The validator can only send NEW_STAKE and RECOVER_STAKE to the elector. In case the validator private key was compromised, the validator address can be changed. Notice that in this case the funds are safe as only the owner can withdraw the funds.
In case the owner is using a hot wallet (not recommended), change-validator-deeplink.ts can be used to generate a deeplink to change the validator address.
Command line: ts-node scripts/ts/change-validator-deeplink.ts single-nominator-addr new-validator-address
where:
- single-nominator-addr is the single nominator address.
- new-validator-address (defaults to ZERO address) is the address of the new validator. If you want to immediately disable the validator and only later set a new validator it might be convenient to set the validator address to the ZERO address.
The owner should run the deeplink from a phone with tonkeeper wallet.
In case the owner is using a cold wallet (recommended), change-validator.fif can be used to generate a boc body which includes change-validator opcode and the new validator address.
Command line: fift -s scripts/fif/change-validator.fif new-validator-address
.
This script will generate a boc body (named change-validator.boc) that should be signed and send from the owner's wallet.
From the black computer the owner should run:
- create and sign the tx:
fift -s wallet-v3.fif my-wallet single_nominator_address sub_wallet_id seqno amount -B change-validator.boc
where my-wallet is the owner's pk file (without extension). For amount 1 TON should be enough to pay fees (remaining amount will be returned to owner). The change-validator.boc is the boc generated above. - from a computer with access to the internet run:
lite-client -C global.config.json -c 'sendfile wallet-query.boc'
to send the boc file (wallet-query.boc) generated in the prev step.
This opcode is not expected to be used under normal conditions.
It can be used to send any message from the nominator contract (must be signed and sent from owner's wallet).
You might want to use this opcode if, for example, the elector contract address was unexpectedly changed and the funds are still locked in the elector. In this case RECOVER_STAKE from validator will not work and the owner will have to build a specific message.
The message body should include: opcode=0x7702 (32 bits), query_id (64 bits), mode (8 bits), reference to the cell msg which will be sent as a raw message.
This is an emergency opcode and probably should never not be used.
It can be used to upgrade the nominator contract.
The message body should include: opcode=0x9903 (32 bits), query_id (64 bits), reference to the new cell code.
This repo includes a test folder with an e2e test (e2e.ts
) which should be run on testnet. The client used in the e2e test is defined by the two env variables: TON_ENDPOINT and TON_API_KEY, make sure those variables are set to testnet.
To run the e2e use: npm run e2e
. It should deploy the nominator contract and run full e2e test.
This folder also includes a unit test file (single-nominator.test.ts
) which uses the evm-contract-executor.
To run the unit test use: npm run test.