Перейти к основному содержимому

Партнёрка, кешбэк, обгон и компрессия

Покупка земли и покупка вышки начисляют партнёрское вознаграждение четырём владельцам земель выше по дереву земель. Владелец земли может отказаться от части своей доли в пользу покупателя — это кешбэк. Кому именно достанется вознаграждение, определяют обгон и компрессия: пустые места в цепочке пропускаются.

Две цепочки — не путать​

В коде слово «цепочка» означает две разные вещи, и одно и то же имя поля используется для обеих.

Цепочка партнёров Program 1inviterChain — цепочка по дереву земель
Откудаprogram1, уровень 1, getPartnersChaingroundmatrix, getGroundInviterChain
Что в нейВсе аплайны пользователя до uid 14 владельца земель от земли-родителя вверх
Для чегоНайти родителя земли и землю-хозяина вышкиКому начислить партнёрку
ДлинаВся линияРовно 4, дополняется uid 1

Бэкенд кладёт inviterChain в поле ответа partnersChain (apps/backend/src/liquidity/liquidity.service.ts:478, 1103), и под этим же именем оно уходит в контракт. Так что partnersChain в подписи и в контракте — это 4 владельца земель, а не цепочка Program 1.

Линии​

Линий 4 — GROUND_PARTNERS_CHAIN_LENGTH (apps/p2-backend/src/tree.service.ts:47). Контракты умеют до 15 (MAX_PARTNER_LINE), но бэкенд всегда передаёт 4.

ЛинияlineIndex в контрактеКто
10Владелец земли-родителя (для земли) или земли-хозяина (для вышки)
21Владелец её родителя
32Следующий вверх
43Следующий вверх

Если дерево земель кончается раньше четвёртой линии, недостающие линии получает uid 1 (tree.service.ts:2003-2007). У корневых земель тоже uid 1.

Проценты​

Базовые проценты захардкожены в бэкенде — [5, 5, 5, 5], отдельно для земли и для вышки (liquidity.service.ts:1099-1102, 474-477). Контракты считают долю как amount × percent / 10000 (MagnetLiquidityGround.sol:325, MagnetLiquiditySharedOps.sol:854).

5 — это 0,05 %, а не 5 %

Проценты уходят в контракт как есть, а контракт трактует их как базисные пункты. Сейчас каждая линия получает 0,05 % от суммы, все четыре — 0,2 %. Тесты контракта используют 500 — 5 % (apps/contracts-v2/test/MagnetLiquidityGround.test.ts). Нужно решить, какой масштаб задуман, и привести бэкенд к нему.

С учётом кешбэка процент линии считается так (getAdjustedPartnerPercents, liquidity.service.ts:1184-1200):

percent[i] = ceil( base[i] × (100 − cashback[i]) / 100 ),   cashback[i] ∈ [0, 100]

Округление вверх при базе 5 съедает маленький кешбэк:

Кешбэк линииПроцент в контрактДоля
0 %50,05 %
10 %5 (4,5 → 5)0,05 %
19 %5 (4,05 → 5)0,05 %
20 %40,04 %
50 %3 (2,5 → 3)0,03 %
100 %00

Проценты и цепочка входят в подпись бэкенда через partnersHash = keccak256(abi.encode(partnersChain, partnerPercents)), поэтому покупатель не может их подменить.

Кешбэк​

Кешбэк — это часть своей партнёрской доли, которую владелец земли отдаёт покупателю. Хранится на земле в графе двумя массивами по 4 числа (0–100):

  • groundCashback — применяется, когда под владельцем покупают землю;
  • towerCashback — когда покупают вышку.

Элемент i — сколько процентов своей доли владелец отдаёт, когда стоит на линии i + 1 относительно покупателя. При покупке для каждой линии берётся земля партнёра этой линии того же уровня и индекса, и из её массива — элемент с номером этой линии (resolveCashbackByInviterChain, tree.service.ts:97-178).

Куда уходит «отданная» часть, зависит от того, что покупают:

  • Вышка. Доля партнёра меньше — больше netAmount, и разница уходит в ликвидность, то есть в долю покупателя. Это настоящий кешбэк.
  • Земля. Покупатель всё равно платит полную цену, а разница просто остаётся на контракте земли. Покупателю ничего не возвращается.

Настраивает владелец земли: в разделе «Grounds и Towers» на /finances/liquidity клик по своей земле открывает EditGroundCashbackModal, сохранение идёт в POST /liquidity/ground/cashback p2-backend без транзакции. По умолчанию кешбэк нулевой.

configureCashback в контракте не используется

В MagnetLiquidityGround есть configureCashback с процентом, сроком активации и длительностью по линии (MagnetLiquidityGround.sol:33-40, 97-112). Конфиг никто не читает, параметр signature не проверяется, фронт функцию не вызывает. Действующий кешбэк — только в графе, без сроков.

Распределение при покупке земли​

buyGround переводит с покупателя всю сумму amount и для каждой линии записывает долю в partnerRewardsBalance[uid][token][lineIndex] (_distributePartnerRewards, MagnetLiquidityGround.sol:309-330). Токены остаются на контракте до клейма.

Остаток — всё, что не ушло партнёрам, — тоже остаётся на контракте. Функции вывода в контракте нет, получателей «в компанию» тоже (см. известные проблемы).

Сейчас фронт продаёт землю за 0 USDT (LAND_PRICE_USDT), поэтому партнёрка с земель фактически нулевая. Деньги в этом флоу платятся за уровни программ в Diamond (Polygon), и распределяет их партнёрская программа Diamond, а не Land.

Распределение при покупке вышки​

MagnetLiquiditySharedOps.buyNft → _distribute(payToken, payAmount, isBuy = true, …) (MagnetLiquiditySharedOps.sol:837-874):

  1. Доли четырёх линий записываются в partnerRewardsBalance в payToken.
  2. Доли buyDistributionTargets (адреса компании, настраивает админ) переводятся сразу.
  3. Остаток (netAmount) меняется на токены пула и добавляется в ликвидность — из него считается доля NFT.

При клейме дохода вышки партнёрка не начисляется: _claimCore передаёт пустую цепочку, работают только claimDistributionTargets (MagnetLiquiditySharedOps.sol:621-637). ТЗ (tz/active/liquidity-nft.md) предполагало распределение и при клейме — в коде этого нет.

Массивы buyPartnerPercents и claimPartnerPercents из BaseLiquidityStorage использует только MagnetLiquidityPersonal; у общих позиций проценты приходят из подписи.

Балансы и клейм партнёрки​

Оба контракта копят вознаграждение одинаково: partnerRewardsBalance[uid][token][lineIndex]. Раздельные линии нужны, чтобы ограничивать, с каких линий пользователь может забрать вознаграждение. Посмотреть баланс — pendingPartnerRewards(uid, tokens, maxLines).

MagnetLiquidityGroundMagnetLiquidityShared
ВызовclaimPartnerRewards(tokens, maxLines)claimPartnerRewards(tokens, maxLines, nonce, deadline, sig)
ПодписьНе нужнаEIP-712 ClaimPartnerPermit(maxLines, nonce, deadline) от signatureVerifier
Кто ограничивает линииСам пользователь (maxLines, 0 = все 15)Бэкенд — через maxLines в подписи
ПолучательPartnerChainBridge.userAddresses(uid)PartnerChainBridge.userAddresses(uid)

uid определяется как PartnerChainBridge.userIds(msg.sender), а выплата идёт на актуальный адрес uid в мосте. Поэтому после смены кошелька (updateUserAddress в мосте) вознаграждение придёт на новый адрес.

Сейчас партнёрку не забрать
  • На фронте нет ни просмотра, ни клейма: строка «Партнёрка к клейму» всегда 0 (apps/frontend/src/widgets/liquidityManagement/model/liquidityManagement.effects.ts:272-278).
  • Для MagnetLiquidityShared бэкенд не выдаёт подпись ClaimPartnerPermit — в бэкендах нет эндпоинта, так что вознаграждение с вышек не заберёт даже прямой вызов контракта.
  • MagnetLiquidityGround клейм без подписи позволяет, но ограничение по линиям там не действует.

Обгон и компрессия​

Обгон в программах Diamond​

Уровни земли привязаны к уровням программ: землю уровня L можно купить только с уровнем L в Program 1 и Program 2. Уровни программ покупаются с обгоном:

  • Program 1: при покупке уровня L пользователь встаёт под ближайшего аплайна, у которого уровень Program 1 не ниже L (findP1OvertakinParent, apps/contracts-v2/contracts/diamond/libs/CoreLib.sol:373-386). Пригласитель без этого уровня «обогнан» и пропускается.
  • Program 2: то же по уровням Program 2 (findP2OvertakinParent, CoreLib.sol:388-397).
  • Каскад Program 1: каждый третий реферал матрицы закрывает её, и её владелец переходит в матрицу своего родителя (setP1UserPos, CoreLib.sol:311-347).

Землям нужен только первый уровень Program 1 (для цепочки) и сами уровни программ (для допуска к покупке). Обгон на уровнях 2–15 на земли напрямую не влияет.

Компрессия при размещении земли​

Новая земля встаёт под землю ближайшего аплайна, у которого есть земля того же уровня и индекса. Аплайны без такой земли пропускаются, в конце цепочки — uid 1. Подробно — в дереве земель.

Компрессия по местам при покупке вышки​

Вышка встаёт на землю ближайшего аплайна, у которого на земле того же уровня и индекса есть свободное место. Заполненные земли пропускаются — «перелив» вверх. Подробно — в выборе земли-хозяина.

Компрессия в выплатах​

Партнёрку получают владельцы земель на пути по дереву земель от земли-родителя (или земли-хозяина) вверх. Кто был пропущен при размещении, в этот путь не попадает и с этой покупки ничего не получает. Линии, которых не хватило до четвёртой, получает uid 1.

Возвратов нет​

Решения о размещении принимаются один раз и не пересматриваются:

  • если пропущенный пригласитель позже купит землю нужного уровня, его даунлайн к нему не вернётся — связи UPLINE_OF/DOWNLINE_OF не меняются;
  • вышка не переезжает, когда на земле ближайшего аплайна освобождается место (места и не освобождаются: продажи и сжигания вышки граф не отслеживает);
  • трансфер NFT земли не меняет владельца в графе и получателя партнёрки.

Единственный «возврат» в системе — кешбэк: владелец земли сам отдаёт покупателю часть своей доли.

Пример​

Цепочка пригласителей: uid 1 → A (10) → B (20) → C (30) → D (40). Земли уровня 2 с индексом 0 есть у A и C, у B — нет.

  1. C покупал землю раньше: ближайший аплайн с землёй — A (B пропущен), значит родитель земли C — земля A.
  2. D покупает землю уровня 2. Цепочка Program 1 по убыванию uid — [30, 20, 10, 1]; первая с землёй — C. Родитель — земля C.
  3. inviterChain по дереву земель от земли C: [C, A, 1, 1] — у A родитель корневая земля uid 1, дальше дерево кончается и массив дополняется uid 1.
  4. Партнёрка: линия 1 — C, линия 2 — A, линии 3 и 4 — uid 1. B не получает ничего, хотя стоит между C и A в линии пригласителей.
  5. Если у C в groundCashback стоит [50, 0, 0, 0], процент линии 1 будет ceil(5 × 50 / 100) = 3, остальные — 5.

PartnerChainBridge: uid и адрес в BSC​

Контракты земли и вышек живут в BSC, а пользователи и их uid — в Diamond в Polygon. Связку переносит PartnerChainBridge (описание контракта):

  • MagnetLiquidityGround берёт uid покупателя из userIds(msg.sender) — без записи в мосте подпись не сойдётся;
  • оба контракта платят партнёрку на userAddresses(uid).

Мост наполняет p2-backend:

  1. TreeEventService (apps/p2-backend/src/tree.event.service.ts) каждые 30 с читает события Diamond RegistrationInPrograms, BuyP1Level, BuyP2Level.
  2. checkPartnersAndReferralsChain (tree.event.service.ts:121-219) проверяет, записана ли в мосте цепочка пользователя на этом уровне. Если нет — берёт getUserPartners(uid, level) из Diamond и кладёт задание в Redis-стрим p2:set-partners-chain.
  3. SetPartnersChainBridgeStreamProcessor (apps/p2-backend/src/jobs/setPartnersChainBridge.job.ts) вызывает setUserPartnersChain(uid, address, level, chain) — он и записывает userIds/userAddresses, — а при наличии ещё и setUserReferralChain. Ключ оператора — LIQUIDITY_CHAIN_BRIDGE_OPERATOR_PRIVATE_KEY.

Цепочки партнёров из моста (15 uid на уровень) Land не использует — партнёрка земель и вышек идёт по дереву земель из Neo4j. Цепочки моста читает MagnetLiquidityPersonal.