Access control: только владелец может вызвать метод

Ограничишь admin-методы проверкой owner в Tact и FunC и избежишь «открытого» setCode/withdraw.

Access control

Любой метод, который меняет критичное состояние (owner, код, вывод средств, параметры экономики), обязан проверять, что sender() — уполномоченный адрес. Без проверки «админка» доступна всей сети.


Уязвимо: withdraw без owner

tact (fallback highlight)
// @tact-lang/compiler 1.6.13
// УЯЗВИМО

message SetOwner {
    next: Address;
}

contract TreasuryBad {
    receive(msg: SetOwner) {
        // любой перепишет владельца
        // (в этом сниппете owner даже не хранится — только демонстрация дыры)
    }

    receive("drain") {
        send(SendParameters{
            to: sender(),
            value: 0,
            mode: SendRemainingBalance,
            bounce: false,
        });
    }
}
func (fallback highlight)
;; FunC (func 0.4.6) — УЯЗВИМО: нет сравнения с owner в storage
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
  slice cs = in_msg_full.begin_parse();
  cs~load_uint(4);
  slice sender_addr = cs~load_msg_addr();
  ;; дальше — действие без проверки sender_addr == owner
}

Исправлено: owner в storage + require

tact (fallback highlight)
// @tact-lang/compiler 1.6.13
// ИСПРАВЛЕНО

message SetOwner {
    next: Address;
}

message Withdraw {
    amount: Int as coins;
}

contract TreasuryOk(
    owner: Address,
) {
    receive() { cashback(sender()) }

    receive(msg: SetOwner) {
        require(sender() == self.owner, "not owner");
        self.owner = msg.next;
        cashback(sender());
    }

    receive(msg: Withdraw) {
        require(sender() == self.owner, "not owner");
        require(msg.amount > 0, "zero");
        send(SendParameters{
            to: self.owner,
            value: msg.amount,
            mode: SendPayGasSeparately,
            bounce: true,
        });
    }

    get fun ownerAddress(): Address {
        return self.owner;
    }
}
func (fallback highlight)
;; FunC (func 0.4.6) — ИСПРАВЛЕНО
(slice owner) load_data() inline {
  slice ds = get_data().begin_parse();
  return (ds~load_msg_addr());
}

() save_data(slice owner) impure inline {
  set_data(begin_cell().store_slice(owner).end_cell());
}

() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
  slice cs = in_msg_full.begin_parse();
  cs~load_uint(4);
  slice sender_addr = cs~load_msg_addr();
  slice owner = load_data();
  throw_unless(401, equal_slices(sender_addr, owner));
  ;; дальше — только авторизованная логика
}

Как это работает

  • owner задаётся в init / fromInit и хранится в data; смена — отдельный метод с той же проверкой.
  • Для ролей (admin, pauser, minter) храни несколько адресов или map; не смешивай «любой user-flow» с admin-opcode в одном handler без ветки.
  • Проверка sender(), а не поля вроде owner внутри body: body подделывается.
  • Multisig / dao как owner — отдельный контракт-адрес; тогда owner указывает на него.

Частые ошибки

Проверяешь owner только на одном из трёх admin-методов → атакующий идёт в незакрытый.

Оставляешь demo-owner из туториала в mainnet init → чужой ключ из README.

Сравниваешь с myAddress() вместо owner → бессмыслица; нужен явный уполномоченный адрес.


Что дальше

Материалы gramdocs.tech носят образовательный характер и не являются финансовой, юридической или инвестиционной рекомендацией. Работа с блокчейном TON и токеном Gram связана с рисками потери средств. Правовая информация