Skip to content

ton-org/royalty-distributor

Repository files navigation

Royalty Distributor

This contract is a proxy distributor that distributes TON based on predefined shares in a dictionary that is set during contract initialization and remains unchanged.

The contract does NOT perform distribution in the following cases:

  1. When the Deploy message with an opcode of 0x7a2f11d8 is received
  2. When the contract balance is less than DISTRIBUTION_BUFFER_LIMIT
  3. When there is insufficient gas for distribution

The dictionary represents a list of recipients with their shares. Dictionary criteria:

  1. No more than 10 addresses and no fewer than 1 address
  2. The sum of shares must be exactly 100.00%
  3. The precision ROYALTY_PRECISION = 1000 means that every percentage value is stored multiplied by 1000 (so you can express fractions down to 0.001%). For example, a 70 % share is saved as 70_000, while 12.345 % becomes 12_345.

If the dictionary meets all criteria, the funds are distributed according to the specified shares. If any of the criteria are not met, that contract instance is considered incorrect and all incoming funds will be sent to the trustedBackupAddress.

Contract Interface

TL-B

Deploy

deploy#7a2f11d8 = Deploy;

Storage

_ isInitialized:Bool royaltyDict:(Maybe ^Cell) trustedBackupAddress:MsgAddress totalRoyaltyReceivers:(Maybe uint8) = Storage;

Get Methods

There is only one get method, which allows you to fetch the internal contract state.

Its signature is getStorage(), and it returns a stack with the following struct:

struct Storage {
    isInitialized: bool
    royaltyDict: RoyaltyDict
    trustedBackupAddress: address
    totalRoyaltyReceivers: uint8?
}

Field descriptions:

  • isChecked – validation flag. It MUST be set to false at deployment time; once all checks succeed (dictionary size, share sums, minimal lump fee, etc.) the contract flips it to true. While isChecked is false, every incoming TON is forwarded to trustedBackupAddress without attempting distribution.
  • royaltyDict – The royalty distribution dictionary (map<address_id, share>). The key is address_id (the 256-bit hash of a workchain-0 address), the value is the participant's share encoded as uint32 with 1000 precision (e.g. 70 000 = 70 %).
  • trustedBackupAddress – fallback address. Any funds sent to an incorrect contract instance (when isChecked is false) or other unrecoverable situations will be forwarded here.
  • totalRoyaltyReceiversuint8? that, after initialization, holds the actual number of receivers defined in royaltyDict. During deployment this field may be used as an arbitrary contract identifier (for example 0). If the value is absent (null), deployment validation has failed.

You can check parsing examples in wrappers.

Get Method description:

getStorage() is a view (get) method that returns the full on-chain state of a particular Distributor instance in a single stack record.

Stack layout that the method returns (top → bottom):

  1. isChecked0 / 1 bit.
  2. royaltyDict – reference to the dictionary cell or null.
  3. trustedBackupAddress – address.
  4. totalRoyaltyReceiversuint8 or null.

Consumers are expected to parse the dictionary according to the key/value format of the royaltyDict, which is described above in the field descriptions.

Deployment

You can deploy the contract using scripts/deployDistributor.ts. For this, you will need to:

  1. Modify the royaltyDict with desired values where each element consists of:
    • Address
    • Share in percentage multiplied by 1,000 (uint32)
const royaltyDict: RoyaltyDistributionParams = [
    {
        // should be in workchain 0
        address: Address.parse('royalty-receiver-address'),
        royaltyPercent: 70_000n, // 70%
    },
    {
        address: Address.parse('another-royalty-address'),
        royaltyPercent: 30_000n, // 30%
    },
];
  1. Set the trustedAddress to your desired address

Run:

npm run start

After deployment, you will receive a message about the contract's correctness.

Language-agnostic initialization

If you are deploying the contract from a language other than TypeScript, craft the initial data cell according to the schema (TL-B).

Example (pseudo-code):

cell data = beginCell()
    .storeBit(0) ; isChecked = false
    .storeMaybeCell(royaltyDict)
    .storeAddress(trustedBackupAddress)
    .storeMaybeUint(null, 8) ; totalRoyaltyReceivers
    .endCell();

Any SDK (Python, Go, etc.) that supports cell building can reproduce the snippet above. The only requirement is to construct the royaltyDict using the proper key/value format and store it as a reference.

Receiving Funds

The contract accepts incoming transfers from any address with any payload. To distribute funds (pay royalty), you should simply send a message with the royalty amount as value to this contract address and it will handle the rest. There is no need to filter out low values, since the contract uses buffer distribution logic. For instance, if the buffer limit is 1 TON and the contract’s current balance is 0.6 TON, sending an additional 0.2 TON will not trigger distribution immediately—the funds will be cached inside the contract. Once the balance crosses the 1 TON threshold (e.g. after another incoming transfer), the full amount will be distributed according to royaltyDict.

Who pays for gas? During distribution the contract purchases compute-phase gas using the TONs that arrived with the triggering message (msgValue). However, the forward fees (fwdFee) for every outgoing royalty transfer are deducted from the contract’s own balance during the action phase. Although:

• The sender must attach enough value to cover compute gas plus the lump forward fee for each receiver. (≈0.0128 TON) • The royalty recipients receive their shares in full; forward fees are not subtracted from the values they receive.

If msgValue is too small to buy the required gas, the contract simply keeps the funds until a larger top-up arrives (same buffer logic as above).

You can create multiple instances that share the same royaltyDict by adjusting the totalRoyaltyReceivers parameter. This value will be overwritten during contract initialization. The contract itself doesn't have any "admin" custodial functionality, meaning that to change royalty distribution parameters you will need to deploy a new instance and change the royalty receiver address.

Project Structure

  • contracts - Source code of all the smart contracts in the project and their dependencies.
  • wrappers - Wrapper classes (implementing Contract from ton-core) for the contracts, including any [de]serialization primitives and compilation functions. Warning: if you change the contract code or its TL-B schema, make sure to update these wrappers manually—they are not generated automatically.
  • tests - Tests for the contracts.
  • scripts - Scripts used by the project, mainly the deployment scripts.
  • utils - Utility helpers used across tests and deployment:
    • gasUtils.ts – functions to parse blockchain config, adjust gas and storage prices, and calculate message forward fees.
    • setup.ts – helpers for unit tests: royalty dictionary generation/validation and on-chain deployment setup for the Distributor contract.
    • tvm11.ts – convenience helper to activate TVM v11 capabilities in the local Sandbox blockchain.

Required Software

  • Node.js >= 18 (LTS recommended)
  • npm >= 9 (comes bundled with Node.js)

Commands

Install dependencies:

npm install

Build:

npm run build

Test:

npm run test

Run scripts / deploy:

npm run start

License

This project is licensed under the MIT License.

About

Royalty distributor contract

Topics

Resources

License

Stars

Watchers

Forks

Contributors