From fc83a7d79bd42714887b41769c73eb7761a150ec Mon Sep 17 00:00:00 2001 From: Tim McMackin Date: Tue, 15 Jul 2025 15:53:17 -0400 Subject: [PATCH 1/2] fix(docs): clarify security topic and add JsLIGO examples --- gitlab-pages/docs/advanced/security.md | 472 ++++++++++++++++++ .../docs/advanced/src/security/bank.jsligo | 26 + .../docs/advanced/src/security/bank.mligo | 17 + .../src/security/rewardswithflaw.jsligo | 41 ++ .../src/security/rewardswithflaw.mligo | 32 ++ .../src/security/walletwithflaw.jsligo | 55 ++ .../src/security/walletwithflaw.mligo | 42 ++ gitlab-pages/docs/intro/ligo-intro.md | 2 +- .../docs/syntax/contracts/entrypoints.md | 2 +- .../docs/tutorials/security/security.md | 251 ---------- .../security/src/security/ungrouped.mligo | 65 --- gitlab-pages/website/sidebars.js | 2 +- 12 files changed, 688 insertions(+), 319 deletions(-) create mode 100644 gitlab-pages/docs/advanced/security.md create mode 100644 gitlab-pages/docs/advanced/src/security/bank.jsligo create mode 100644 gitlab-pages/docs/advanced/src/security/bank.mligo create mode 100644 gitlab-pages/docs/advanced/src/security/rewardswithflaw.jsligo create mode 100644 gitlab-pages/docs/advanced/src/security/rewardswithflaw.mligo create mode 100644 gitlab-pages/docs/advanced/src/security/walletwithflaw.jsligo create mode 100644 gitlab-pages/docs/advanced/src/security/walletwithflaw.mligo delete mode 100644 gitlab-pages/docs/tutorials/security/security.md delete mode 100644 gitlab-pages/docs/tutorials/security/src/security/ungrouped.mligo diff --git a/gitlab-pages/docs/advanced/security.md b/gitlab-pages/docs/advanced/security.md new file mode 100644 index 000000000..0495b9eb0 --- /dev/null +++ b/gitlab-pages/docs/advanced/security.md @@ -0,0 +1,472 @@ +--- +id: security +title: Smart contract security +--- + +import Syntax from '@theme/Syntax'; + +Web3 developers need to keep some specific vulnerabilities in mind when they write on-chain and off-chain applications. +This page covers the basics of smart contract security on Tezos, some of these potential vulnerabilities, and how to protect your contracts against them. + +:::note + +This guide is aimed at giving the reader an overview of popular attacks on smart contracts and distributed applications. +It is not an exhaustive list of all the possible attack vectors. +Use your own judgement, good programming practice, and thorough testing on your contracts. + +The descriptions in this document are valid for the Tezos protocol since the Edo upgrade, last updated at the Rio upgrade. +Because Tezos is an upgradeable blockchain, some of the blockchain mechanics may change when new protocols are adopted. +For this reason, Tezos developers must stay up to date on the changes in the protocol via sources such as the Octez and protocol documentation at https://octez.tezos.com. + +::: + +See these links for more information about security on Tezos applications: + +- The smart contracts section on https://opentezos.com +- The tutorial [Learn and play with security](https://docs.tezos.com/tutorials/security) on docs.tezos.com + +## Resource constraints + +Tezos limits the size of an operation so that nodes can broadcast operations over the network in a reasonable time. +It also places a limit on the computations that bakers need to perform to validate an operation to keep the network running smoothly. +This limit is called the *gas limit* because it is the maximum amount of computations (measured in *gas units*) that a single operation can require. + +Of course, developers make their contracts efficient to save on gas fees, but they must also keep the gas limit in mind because it can lead to security vulnerabilities. + +For example, look at this seemingly innocent wallet contract that stores an event log: + + + +```cameligo group=walletwithflaw +module WalletWithFlaw = struct + + (* Variant for two types of transactions *) + type transaction = + Deposit of address * tez + | Withdrawal of address * tez + + type storage = { + owner : address; + transactionLog : transaction list + } + + type return_type = operation list * storage + + (* Receive a deposit *) + [@entry] + let deposit (_ : unit) (storage : storage) : return_type = + (* Verify that tez was sent *) + let _ = if Tezos.get_amount () = 0tez then failwith "Send tez to deposit" in + (* Add log entry *) + let newLogEntry : transaction = Deposit (Tezos.get_sender (), Tezos.get_amount ()) in + [], { storage with transactionLog = newLogEntry :: storage.transactionLog } + + (* Return a withdrawal *) + [@entry] + let withdraw (tx_destination, tx_amount : address * tez) (storage : storage) : return_type = + (* Verify that the sender is the admin *) + let _ = if Tezos.get_sender () <> storage.owner then failwith "Not the owner" in + (* Verify that no tez was sent *) + let _ = if Tezos.get_amount () <> 0tez then failwith "Don't send tez to this entrypoint" in + (* Create transaction *) + let callee = Tezos.get_contract_opt tx_destination in + let operation = match callee with + Some contract -> + Tezos.Operation.transaction () tx_amount contract + | None -> failwith "Couldn't send withdrawal to that address" + in + (* Add log entry and return operation and new log *) + let newLogEntry : transaction = Withdrawal (tx_destination, tx_amount) in + [operation], { storage with transactionLog = newLogEntry :: storage.transactionLog } + +end +``` + + + + + +```jsligo group=walletwithflaw +namespace WalletWithFlaw { + + // Variant for two types of transactions + type transaction = + ["Deposit", [address, tez]] + | ["Withdrawal", [address, tez]]; + + type storage = { + owner: address, + transactionLog: list, + }; + + type return_type = [list, storage]; + + // Receive a deposit + // @entry + const deposit = (_: unit, storage: storage): return_type => { + // Verify that tez was sent + if (Tezos.get_amount() == (0 as tez)) { + failwith("Send tez to deposit"); + } + // Add log entry + const newLogEntry: transaction = ["Deposit" as "Deposit", [Tezos.get_sender(), Tezos.get_amount()]]; + return [[], { + owner: storage.owner, + transactionLog: [newLogEntry, ...storage.transactionLog], + }]; + } + + // Return a withdrawal + // @entry + const withdraw = (param : [address, tez], storage : storage): return_type => { + const [tx_destination, tx_amount] = param; + // Verify that the sender is the admin + if (Tezos.get_sender() != storage.owner) { + failwith("Not the owner"); + } + // Verify that no tez was sent + if (Tezos.get_amount() != (0 as tez)) { + failwith("Don't send tez to this entrypoint"); + } + // Create transaction + const callee = Tezos.get_contract_opt(tx_destination); + const operation = $match(callee, { + "Some": contract => (() => Tezos.Operation.transaction(unit, tx_amount, contract))(), + "None": () => failwith("Couldn't send withdrawal to that address"), + }); + // Add log entry and return operation and new log + const newLogEntry: transaction = ["Withdrawal" as "Withdrawal", [tx_destination, tx_amount]]; + return [[operation], { + owner: storage.owner, + transactionLog: [newLogEntry, ...storage.transactionLog], + }]; + } +} +``` + + + +This contract: + +- Can receive funds sent to it via the `Deposit` entrypoint. +- Can send some tez to any account via the `Withdrawal` entrypoint callable by the owner. +- Stores a log of all transactions. + +What can go wrong? +To see the flaw, you need to understand how Tezos processes transactions and what limits it places on them. + +As described above, Tezos puts a limit on the amount of processing that a single transaction can require. +This processing includes loading all non-lazy variables in the contract's storage. +Each variable gets fetched, deserialised, and type-checked each time the contract is called, which requires computation. + +Each time you call this contract, it adds a log entry to the `list` variable in the storage and therefore the storage is larger the next time that you call it. +This design flaw causes two problems: + +- Calling this contract gets more expensive each time you call it +- Eventually the amount of processing required will exceed the maximum for a single transaction and thus it will be impossible to call the contract, making it unusable and locking the tez in it + +Can you think of a way to fix this flaw while retaining the transaction log? +There are several ways, including: + +- Storing the log off the chain or relying on an indexer to get a list of past transactions +- Using a lazy storage type such as a big-map, which is not loaded entirely when the contract is called +- Truncating the log to show only a few recent transactions or otherwise limiting the size of the log + +In this way, you must plan ahead to limit the storage size of contracts as they grow. +Here are some other ways that storage size can cause problems: + +- Unbounded types such as nats, integers, and bytes can become arbitrarily large. +These types are less likely to cause problems than lists and maps but still can. + +- Lambdas in storage can grow or cause data storage issues, so you should never store untrusted lambdas. + +Also, storage size isn't the only way that contracts can exceed the maximum gas and become unusable. +Lambdas or loops in your code can cause vulnerabilities by forcing future transactions to run a large loop or make too many computations, exceeding the gas limit. +You must consider both the storage and the logic of the contract to ensure that it will not exceed the gas limit in the long term. + +## Transaction ordering + +Blockchains use block producers (called *bakers* in Tezos) to put transactions into blocks. +Block producers are free to include or exclude transactions within the blocks they produce and to put transactions in any order. +Transactions run in the order that they are listed in the block, so in certain cases, block producers can manipulate the order of transactions to make a profit or cause a certain effect. + +Also, bakers usually put transactions with higher transaction fees or lower counter values before transactions with lower fees. +Therefore, other actors can sometimes influence transaction ordering for their benefit. + +Manipulating the transaction order like this happens very rarely, but it can cause problems for decentralised finance (DeFi) applications. + +A classic example of a system vulnerable to this kind of attacks is a decentralised exchange with an on-chain orderbook. +This exchange accepts orders to buy and sell assets at a certain price and runs them in the order that it receives them, which depends on the order that the transactions are listed in each block. + +In an attack known as *front-running*, an attacker may see a large transaction coming and use the methods described above to insert their transaction before that large transaction. +For example, they could see a large buy order coming and submit their own buy order with a high transaction fee to get it to run before the large one raises the price of the asset. +In fact, if the front-runner is a baker, the so-called _miner extracted value_ [poses a big risk](https://arxiv.org/pdf/1904.05234.pdf) to security of blockchains in general. + +To defend against this kind of attack, you must prevent block producers and other users from profiting by manipulating the order of transactions. +For example, you could store the order book off-chain or use [timelocks](https://docs.tezos.com/smart-contracts/timelocks) to prevent attackers from seeing incoming transactions. + +## Timestamps + +Aside from transaction ordering, block producers can manipulate other variables that contracts rely on. +For example, block producers set the timestamp of each block that they create based on their own clocks. +In older versions of the Tezos protocol, the value of the `Tezos.get_now` value was this timestamp. +If a contract used the value of `Tezos.get_now` to get the time, block producers could manipulate the timestamp to change the behaviour of the contract. + +In the current Tezos protocol, the value of `Tezos.get_now` is always the timestamp of the previous block plus a fixed value, regardless of the time that the block was actually created, which eliminates straightforward manipulations. +However, contracts that rely on timestamps are still vulnerable to manipulation. +In particular, contracts should never use timestamps or `Tezos.get_now` as a source of randomness. + +## Reentrancy and call injection + +As described in [Operations](../syntax/contracts/operation) and in [Operations](https://docs.tezos.com/smart-contracts/logic/operations) on docs.tezos.com, Tezos orders operations in a way that may appear unusual to developers who are unfamiliar with blockchains. +In particular, when an operation (such as a call to a smart contract) generates other operations, those operations do not run until the original operation completes. +In general, calls to smart contracts run in this order: + +1. The smart contract runs its logic. +1. The smart contract returns any operations and events that it created and the new state of its storage. +1. The protocol updates the contract's storage based on the storage that it returned. +1. The protocol queues the operations and events that the smart contract returned and runs them in order. + +Note that, based on this order, a smart contract cannot run operations or emit events in the middle of its own execution. +It can only queue operations and events to run after it finishes running. + +See [Operations](https://docs.tezos.com/smart-contracts/logic/operations) on docs.tezos.com for examples of operation ordering. + +This process is similar to the checks-effects-interactions pattern popular in Solidity. +In Ethereum, this process is considered a best practice, and Tezos enforces this on the protocol level with operation ordering. +Such restrictions help prevent *reentrancy attacks*, which take advantage of flaws in contracts by calling them at intermediate points in their execution. + +Consider the following snippet in Solidity from a bank smart contract: + +```solidity +function withdraw(uint256 amount) { + uint256 balance = balances[beneficiary]; + require(balance >= amount); + beneficiary.call.value(amount)(); + uint256 new_balance = balance - amount; + balances[beneficiary] = new_balance; +} +``` + +This code follows these general steps: + +1. A caller requests to withdraw funds from their account. +1. The smart contract checks that the requested amount is less than or equal to their bank balance. +1. The smart contract sends the withdrawn funds to the caller. +1. The smart contract updates the caller's balance in storage. + +Note that the _effect_ of updating the storage happens after the _interaction_ (transferring the `amount` to the beneficiary). +As a result, this contract has a reentrancy vulnerability: If the contract execution pauses after the transfer starts but before the contract updates the caller's balance in storage, the caller could start another withdrawal and receive more funds before the first balance update happens. + +The way that Tezos orders operations makes it difficult to run reentrancy attacks. +For example, here is an equivalent contract in LIGO: + + + +```cameligo group=bank +type storage = (address, tez) big_map +type return_type = operation list * storage + +[@entry] +let withdraw (tx_amount : tez) (storage : storage) : return_type = + (* Verify that the caller has enough balance for the withdrawal *) + let old_balance = Big_map.find (Tezos.get_sender ()) storage in + let _ = if tx_amount > old_balance then failwith "Insufficient balance" in + (* Create transaction *) + let receiver_account = match Tezos.get_contract_opt (Tezos.get_sender ()) with + Some account -> account + | None -> failwith "Couldn't find account" in + let operation = Tezos.Operation.transaction unit tx_amount receiver_account in + (* Update balance *) + let new_balance : tez = Option.value_with_error "Unreachable error; we already compared balance to amount" (old_balance - tx_amount) in + let new_storage = Big_map.update (Tezos.get_sender ()) (Some new_balance) storage in + [operation], new_storage +``` + + + + + +```jsligo group=bank +namespace Bank { + + type storage = big_map; + type return_type = [list, storage]; + + // Return a withdrawal + // @entry + const withdraw = (tx_amount: tez, storage: storage): return_type => { + // Verify that the caller has enough balance for the withdrawal + const old_balance = Big_map.find(Tezos.get_sender(), storage); + if (tx_amount > old_balance) { + failwith("Insufficient balance"); + } + // Create transaction + const receiver_account = $match((Tezos.get_contract_opt(Tezos.get_sender())), { + "Some": account => account, + "None": () => failwith("Couldn't find account"), + }); + const operation = Tezos.Operation.transaction(unit, tx_amount, receiver_account); + // Update balance + const new_balance: tez = Option.value_with_error("Unreachable error; we already compared balance to amount", old_balance - tx_amount); + const new_storage = Big_map.update(Tezos.get_sender(), ["Some" as "Some", new_balance], storage); + return [[operation], new_storage]; + } + +} +``` + + + +The general steps of the code are similar, but the critical difference is that the operation to transfer funds (the `op` operation variable) is created but does not run immediately. +The smart contract returns it in the list of operations at the end of its execution and Tezos queues it to run later. +Tezos updates the balances in the contract storage before it runs any subsequent operations, preventing reentrancy attacks. + +However, in some cases reentrancy attacks are still possible on Tezos, especially if contracts wait for a callback in an intermediate state. +For example, if the bank contract stores balances in a separate contract and updates them by sending transactions to that contract, it may be susceptible to reentrancy attacks. +Users may be able to manipulate the order of those transactions to run more than one withdrawal before the transactions update their balance. + +## Transactions to untrusted contracts + +When emitting a transaction to an untrusted contract, you can not assume that it will "play by the rules." +Instead, you should always bear in mind that the callee may fail, causing the entire operation to fail or emit other operations that you do not expect. + +Consider this example, which keeps a list of addresses. +When the owner account calls the `send_rewards` entrypoint, it attempts to send 5 tez to each address: + + + +```cameligo group=rewardswithflaw +module RewardsWithFlaw = struct + + type storage = { + owner : address; + beneficiaries : address list + } + + (* Send rewards to one address *) + let send_one_reward (beneficiary_addr : address) : operation = + let contract_opt = + Tezos.get_contract_opt beneficiary_addr in + let beneficiary = + match contract_opt with + Some contract -> contract + | None -> (failwith "CONTRACT_NOT_FOUND" : unit contract) in + Tezos.Operation.transaction () 5tez beneficiary + + (* Send rewards to all beneficiaries *) + [@entry] + let send_rewards (_ : unit) (storage : storage) : operation list * storage = + if Tezos.get_sender () <> storage.owner + then failwith "Not the owner" + else let operations = List.map send_one_reward storage.beneficiaries in + operations, storage + + [@entry] + let change_owner (new_owner : address) (storage : storage) : operation list * storage = + (* Verify that the sender is the admin *) + let _ = if Tezos.get_sender () <> storage.owner then failwith "Not the owner" in + [], { storage with owner = new_owner } + +end +``` + + + + + +```jsligo group=rewardswithflaw +namespace RewardsWithFlaw { + + export type storage = { + owner: address, + beneficiaries: list
, + }; + + // Send rewards to one address + const send_one_reward = (beneficiary_addr: address): operation => { + const contract_opt = + Tezos.get_contract_opt( beneficiary_addr); + const beneficiary = $match(contract_opt, { + "Some": contract => contract, + "None": () => failwith("CONTRACT_NOT_FOUND"), + }); + return Tezos.Operation.transaction(unit, 5 as tez, beneficiary); + } + + // Send rewards to all beneficiaries + // @entry + const send_rewards = (_: unit, storage: storage): [list, storage] => { + if (Tezos.get_sender() != storage.owner) { + failwith("Not the owner"); + } + const operations = List.map(send_one_reward, storage.beneficiaries); + return [operations, storage]; + } + + // @entry + const change_owner = (new_owner: address, storage: storage): [list, storage] => { + // Verify that the sender is the admin + if (Tezos.get_sender() != storage.owner) { + failwith("Not the owner"); + } + return [[], { + owner: new_owner, + beneficiaries: storage.beneficiaries, + }]; + } + +} +``` + + + +When the owner calls the `send_rewards` entrypoint, the contract attempts to create a list of operations. +If one of these attempts to create an operation fails because the receiving contract fails or does not exist, the entire call to the `send_rewards` entrypoint fails and no transfers happen. +Regardless of whether this failure is because of a mistake or intentional censorship, the contract is stuck. + +In cases like these, allow users to withdraw funds independently instead of running operations in a batch. +This way, if one transfer fails, it does not affect other transfers. + +## Incorrect authorisation checks + +When developing a contract, you may want to restrict who can call a certain entrypoint. +In this case you must ensure that: + +- The request comes from an authorised entity +- The authorised entity cannot be tricked into sending the request + +To determine which account sends a request, you may be tempted to use the `Tezos.get_source` function. +This function returns the address of the account that submitted the operation that started a chain of operations, but not necessarily the account that sent the immediate transaction that the contract is running now. +Relying on `Tezos.get_source` can allow a malicious contract to impersonate an account when it calls another contract. + +For example, assume that account A calls smart contract B, which generates an operation to call smart contract C. +When C runs, in Tezos terms, account A is the *source* of the transaction and smart contract B is the *sender* of the transaction. +Therefore, if it uses `Tezos.get_source` to check which account calls it, contract B could trick it into thinking account A called it. +In this way, a malicious contract can invite accounts to make seemingly innocent transactions and use the operation chain to impersonate those users in other transactions. + +:::warning + +For this reason, contracts should never use `Tezos.get_source` for authorisation purposes. + +::: + +For more information about senders and sources, see [Sender vs Source confusion](https://docs.tezos.com/tutorials/security/part-1#sender-vs-source-confusion) on docs.tezos.com. + +Checking whether `Tezos.get_sender` (the address of the immediate caller) is authorised to perform an operation is better. +Because the request comes directly from an authorised entity, contracts can be more confident that the call is legitimate. +This approach is a good default choice if both conditions hold true: + +1. The sender contract is well secured against emitting arbitrary operations. +For instance, it must not contain ["view" entrypoints](https://gitlab.com/tzip/tzip/-/blob/master/proposals/tzip-4/tzip-4.md#view-entrypoints) as defined in [TZIP-4](https://gitlab.com/tzip/tzip/-/blob/master/proposals/tzip-4/tzip-4.md). + +2. You only need to authorise an immediate caller and not the contracts somewhere up in the call chain. + +If either of these conditions is not met, you may need to use [tickets](../data-types/tickets) to authenticate requests. +Tickets can be transferred, but they always have the address of the contract that created them as their ticketer. + +In this way, tickets allow you to verify that requests came from a certain contract. +For example, you can set up a contract that authenticates requests by requiring a ticket with the request, using the address of the ticketer to determine the permissions for the action, and using the data in the ticket as the parameters or input for the request. + +In general, sender-based authorisation is appropriate only for simple scenarios, such as when the contract has a single "owner" address controlled by a user account. +In more complex scenarios, ticket-based authorisation is often better. diff --git a/gitlab-pages/docs/advanced/src/security/bank.jsligo b/gitlab-pages/docs/advanced/src/security/bank.jsligo new file mode 100644 index 000000000..eeed14428 --- /dev/null +++ b/gitlab-pages/docs/advanced/src/security/bank.jsligo @@ -0,0 +1,26 @@ +namespace Bank { + + type storage = big_map; + type return_type = [list, storage]; + + // Return a withdrawal + // @entry + const withdraw = (tx_amount: tez, storage: storage): return_type => { + // Verify that the caller has enough balance for the withdrawal + const old_balance = Big_map.find(Tezos.get_sender(), storage); + if (tx_amount > old_balance) { + failwith("Insufficient balance"); + } + // Create transaction + const receiver_account = $match((Tezos.get_contract_opt(Tezos.get_sender())), { + "Some": account => account, + "None": () => failwith("Couldn't find account"), + }); + const operation = Tezos.Operation.transaction(unit, tx_amount, receiver_account); + // Update balance + const new_balance: tez = Option.value_with_error("Unreachable error; we already compared balance to amount", old_balance - tx_amount); + const new_storage = Big_map.update(Tezos.get_sender(), ["Some" as "Some", new_balance], storage); + return [[operation], new_storage]; + } + +} \ No newline at end of file diff --git a/gitlab-pages/docs/advanced/src/security/bank.mligo b/gitlab-pages/docs/advanced/src/security/bank.mligo new file mode 100644 index 000000000..8efd2dd04 --- /dev/null +++ b/gitlab-pages/docs/advanced/src/security/bank.mligo @@ -0,0 +1,17 @@ +type storage = (address, tez) big_map +type return_type = operation list * storage + +[@entry] +let withdraw (tx_amount : tez) (storage : storage) : return_type = + (* Verify that the caller has enough balance for the withdrawal *) + let old_balance = Big_map.find (Tezos.get_sender ()) storage in + let _ = if tx_amount > old_balance then failwith "Insufficient balance" in + (* Create transaction *) + let receiver_account = match Tezos.get_contract_opt (Tezos.get_sender ()) with + Some account -> account + | None -> failwith "Couldn't find account" in + let operation = Tezos.Operation.transaction unit tx_amount receiver_account in + (* Update balance *) + let new_balance : tez = Option.value_with_error "Unreachable error; we already compared balance to amount" (old_balance - tx_amount) in + let new_storage = Big_map.update (Tezos.get_sender ()) (Some new_balance) storage in + [operation], new_storage \ No newline at end of file diff --git a/gitlab-pages/docs/advanced/src/security/rewardswithflaw.jsligo b/gitlab-pages/docs/advanced/src/security/rewardswithflaw.jsligo new file mode 100644 index 000000000..01ba72fcd --- /dev/null +++ b/gitlab-pages/docs/advanced/src/security/rewardswithflaw.jsligo @@ -0,0 +1,41 @@ +namespace RewardsWithFlaw { + + export type storage = { + owner: address, + beneficiaries: list
, + }; + + // Send rewards to one address + const send_one_reward = (beneficiary_addr: address): operation => { + const contract_opt = + Tezos.get_contract_opt( beneficiary_addr); + const beneficiary = $match(contract_opt, { + "Some": contract => contract, + "None": () => failwith("CONTRACT_NOT_FOUND"), + }); + return Tezos.Operation.transaction(unit, 5 as tez, beneficiary); + } + + // Send rewards to all beneficiaries + // @entry + const send_rewards = (_: unit, storage: storage): [list, storage] => { + if (Tezos.get_sender() != storage.owner) { + failwith("Not the owner"); + } + const operations = List.map(send_one_reward, storage.beneficiaries); + return [operations, storage]; + } + + // @entry + const change_owner = (new_owner: address, storage: storage): [list, storage] => { + // Verify that the sender is the admin + if (Tezos.get_sender() != storage.owner) { + failwith("Not the owner"); + } + return [[], { + owner: new_owner, + beneficiaries: storage.beneficiaries, + }]; + } + +} \ No newline at end of file diff --git a/gitlab-pages/docs/advanced/src/security/rewardswithflaw.mligo b/gitlab-pages/docs/advanced/src/security/rewardswithflaw.mligo new file mode 100644 index 000000000..79507d3c2 --- /dev/null +++ b/gitlab-pages/docs/advanced/src/security/rewardswithflaw.mligo @@ -0,0 +1,32 @@ +module RewardsWithFlaw = struct + + type storage = { + owner : address; + beneficiaries : address list + } + + (* Send rewards to one address *) + let send_one_reward (beneficiary_addr : address) : operation = + let contract_opt = + Tezos.get_contract_opt beneficiary_addr in + let beneficiary = + match contract_opt with + Some contract -> contract + | None -> (failwith "CONTRACT_NOT_FOUND" : unit contract) in + Tezos.Operation.transaction () 5tez beneficiary + + (* Send rewards to all beneficiaries *) + [@entry] + let send_rewards (_ : unit) (storage : storage) : operation list * storage = + if Tezos.get_sender () <> storage.owner + then failwith "Not the owner" + else let operations = List.map send_one_reward storage.beneficiaries in + operations, storage + + [@entry] + let change_owner (new_owner : address) (storage : storage) : operation list * storage = + (* Verify that the sender is the admin *) + let _ = if Tezos.get_sender () <> storage.owner then failwith "Not the owner" in + [], { storage with owner = new_owner } + +end \ No newline at end of file diff --git a/gitlab-pages/docs/advanced/src/security/walletwithflaw.jsligo b/gitlab-pages/docs/advanced/src/security/walletwithflaw.jsligo new file mode 100644 index 000000000..baf41555a --- /dev/null +++ b/gitlab-pages/docs/advanced/src/security/walletwithflaw.jsligo @@ -0,0 +1,55 @@ +namespace WalletWithFlaw { + + // Variant for two types of transactions + type transaction = + ["Deposit", [address, tez]] + | ["Withdrawal", [address, tez]]; + + type storage = { + owner: address, + transactionLog: list, + }; + + type return_type = [list, storage]; + + // Receive a deposit + // @entry + const deposit = (_: unit, storage: storage): return_type => { + // Verify that tez was sent + if (Tezos.get_amount() == (0 as tez)) { + failwith("Send tez to deposit"); + } + // Add log entry + const newLogEntry: transaction = ["Deposit" as "Deposit", [Tezos.get_sender(), Tezos.get_amount()]]; + return [[], { + owner: storage.owner, + transactionLog: [newLogEntry, ...storage.transactionLog], + }]; + } + + // Return a withdrawal + // @entry + const withdraw = (param : [address, tez], storage : storage): return_type => { + const [tx_destination, tx_amount] = param; + // Verify that the sender is the admin + if (Tezos.get_sender() != storage.owner) { + failwith("Not the owner"); + } + // Verify that no tez was sent + if (Tezos.get_amount() != (0 as tez)) { + failwith("Don't send tez to this entrypoint"); + } + // Create transaction + const callee = Tezos.get_contract_opt(tx_destination); + const operation = $match(callee, { + "Some": contract => (() => Tezos.Operation.transaction(unit, tx_amount, contract))(), + "None": () => failwith("Couldn't send withdrawal to that address"), + }); + // Add log entry and return operation and new log + const newLogEntry: transaction = ["Withdrawal" as "Withdrawal", [tx_destination, tx_amount]]; + return [[operation], { + owner: storage.owner, + transactionLog: [newLogEntry, ...storage.transactionLog], + }]; + } +} \ No newline at end of file diff --git a/gitlab-pages/docs/advanced/src/security/walletwithflaw.mligo b/gitlab-pages/docs/advanced/src/security/walletwithflaw.mligo new file mode 100644 index 000000000..78777dea0 --- /dev/null +++ b/gitlab-pages/docs/advanced/src/security/walletwithflaw.mligo @@ -0,0 +1,42 @@ +module WalletWithFlaw = struct + + (* Variant for two types of transactions *) + type transaction = + Deposit of address * tez + | Withdrawal of address * tez + + type storage = { + owner : address; + transactionLog : transaction list + } + + type return_type = operation list * storage + + (* Receive a deposit *) + [@entry] + let deposit (_ : unit) (storage : storage) : return_type = + (* Verify that tez was sent *) + let _ = if Tezos.get_amount () = 0tez then failwith "Send tez to deposit" in + (* Add log entry *) + let newLogEntry : transaction = Deposit (Tezos.get_sender (), Tezos.get_amount ()) in + [], { storage with transactionLog = newLogEntry :: storage.transactionLog } + + (* Return a withdrawal *) + [@entry] + let withdraw (tx_destination, tx_amount : address * tez) (storage : storage) : return_type = + (* Verify that the sender is the admin *) + let _ = if Tezos.get_sender () <> storage.owner then failwith "Not the owner" in + (* Verify that no tez was sent *) + let _ = if Tezos.get_amount () <> 0tez then failwith "Don't send tez to this entrypoint" in + (* Create transaction *) + let callee = Tezos.get_contract_opt tx_destination in + let operation = match callee with + Some contract -> + Tezos.Operation.transaction () tx_amount contract + | None -> failwith "Couldn't send withdrawal to that address" + in + (* Add log entry and return operation and new log *) + let newLogEntry : transaction = Withdrawal (tx_destination, tx_amount) in + [operation], { storage with transactionLog = newLogEntry :: storage.transactionLog } + +end \ No newline at end of file diff --git a/gitlab-pages/docs/intro/ligo-intro.md b/gitlab-pages/docs/intro/ligo-intro.md index 3e38a903e..6343974c2 100644 --- a/gitlab-pages/docs/intro/ligo-intro.md +++ b/gitlab-pages/docs/intro/ligo-intro.md @@ -105,7 +105,7 @@ Your choice to learn LIGO is already available: You will need a deeper comprehension: - Teach yourself how to structure your code with [Combining code](../syntax/modules) section - Learn how to [write tests](../testing) we strongly encourage to use [breathalyzer library from the LIGO registry.](https://packages.ligolang.org/package/ligo-breathalyzer) -- Understand how to [secure a contract](../tutorials/security) +- Understand how to [secure a contract](../advanced/security) ### Dig deeper diff --git a/gitlab-pages/docs/syntax/contracts/entrypoints.md b/gitlab-pages/docs/syntax/contracts/entrypoints.md index ee56ce26e..db9976933 100644 --- a/gitlab-pages/docs/syntax/contracts/entrypoints.md +++ b/gitlab-pages/docs/syntax/contracts/entrypoints.md @@ -312,7 +312,7 @@ const owner_only = (action: parameter, storage: storage): result => { :::note The entrypoint in the previous example uses `Tezos.get_sender` instead of `Tezos.get_source` to prevent a security flaw. -For more information, see the [Security tutorial](../../tutorials/security/security.md#incorrect-authorisation-checks). +For more information, see [Security](../../advanced/security). ::: diff --git a/gitlab-pages/docs/tutorials/security/security.md b/gitlab-pages/docs/tutorials/security/security.md deleted file mode 100644 index 08120af2d..000000000 --- a/gitlab-pages/docs/tutorials/security/security.md +++ /dev/null @@ -1,251 +0,0 @@ ---- -id: security -title: Smart contract security ---- - -import Syntax from '@theme/Syntax'; - -In this article, we will cover the basics of Tezos smart contract security. We will describe several potential vulnerabilities that stem from developers' misconceptions about the distributed nature of blockchains. We will also suggest ways to protect your contracts against these kinds of attacks. - -**Disclaimer:** -1. This guide is aimed at giving the reader an overview of popular attacks on smart contracts and distributed applications. It is not an exhaustive list of all the possible attack vectors. Please, use your own judgement. -2. The descriptions in this document are valid for the protocol 008_PtEdo2Zk (Edo). Since Tezos is an upgradeable blockchain, some of the blockchain mechanics may change in case a new proposal is adopted. - -## Resource constraints - -Tezos limits the resources available to the contracts. It bounds operations size so that nodes can broadcast the operations over the network in a reasonable time. It also places a limit on the computations the bakers need to perform to validate an operation – the **gas limit.** When you develop your contract, you need to bear these limits in mind. - -Let us look at a seemingly innocent wallet contract that stores an event log: - - - -```cameligo -type parameter = Fund | Send of address * tez - -type transaction = Incoming of address * tez | Outgoing of address * tez - -type storage = {owner : address; transactionLog : transaction list} - -type result = operation list * storage - -let do_send (dst, @amount : address * tez) = - let callee = Tezos.get_contract_opt dst in - match callee with - Some contract -> - let op = Tezos.Operation.transaction () @amount contract in - Outgoing (dst, @amount), [op] - | None -> (failwith "Could not send tokens" : transaction * operation list) - -let do_fund (from, @amount : address * tez) = - Incoming (from, @amount), ([] : operation list) - -[@entry] -let fund (_ : unit) (s : storage) : result = - let tx, ops = do_fund (Tezos.get_sender (), Tezos.get_amount ()) in - ops, { s with transactionLog = tx :: s.transactionLog } - -[@entry] -let send (args : address * tez) (s : storage) = - let u = Assert.assert ((Tezos.get_sender ()) = s.owner && (Tezos.get_amount ()) = 0mutez) in - let tx, ops = do_send args in - ops, { s with transactionLog = tx :: s.transactionLog } -``` - - - - -This contract: -1. Can receive funds sent to it via the `Fund` entrypoint. -2. Can send some tez via the `Send` entrypoint callable by the owner. -3. Stores a log of all the operations. - -What can go wrong? To answer this question, we will need to dive a bit into how Tezos processes transactions and what limits it places on them. - -To guarantee that the nodes spend reasonable time processing transactions, Tezos requires that the execution consumes no more than a certain amount of _gas_ (in the current protocol, it is 1 040 000 gas units). - -But in Tezos, the amount of gas consumed depends on the size of the storage! All non-lazy (i.e. non-BigMap) storage entries get fetched, deserialised, and type-checked upon each contract invocation. It means that: -1. Our contract will be more and more expensive to call with every transaction made. -2. Eventually, when the gas consumption is too high, every transaction will hit the upper bound, which will render the contract unusable. - -In this particular case the best solution would be to use an off-chain indexer that would monitor and record the transactions to the contract. If you are sure you need an event log in the contract storage, you should at least store the logs in a big map, e.g., indexed incrementally. - -Generally, you need to think about whether the side effect of gas consumption can halt the execution prematurely. Here are the tips that can help you reduce the risk of potential gas exhaustion. -1. Limit the size of non-lazy storage: - - Do not store data extendable by the users (e.g., event logs, a set of token holders) in non-lazy containers. - - If using non-lazy containers is absolutely required, place an upper bound on the size of non-lazy containers. - - Limit the maximum size of strings and byte strings. - - Do not put untrusted lambdas in storage. - - Be careful with all unbounded types, including `nat`, `int`, etc. Although exploiting gas exhaustion attacks with non-container types may be harder, it is still possible. -2. Ensure that your contract logic does not allow attackers to increase the interpretation cost, e.g., by forcing future transactions to run a huge loop. - -## Transaction ordering -It is crucial to understand that all blockchains, including Tezos, are distributed systems where block producers – bakers in Tezos – are free to include, censor, and reorder transactions within a block. For most of the practical applications, this does not pose a threat. However, in some cases, especially in Decentralised Finance (DeFi) applications, bakers can use their power to gain economic benefit from reordering or censoring out user transactions. - -Aside from bakers, other actors can indirectly influence the transaction ordering as well. Attackers can set higher fees or use accounts with lower counter values to make bakers put the attackers' transactions in front of others. - -A classic example of a system vulnerable to this kind of attacks is a decentralised exchange with an on-chain orderbook, like this one (let us assume just one asset pair for clarity): - - - -```cameligo skip -type order = {price : nat; volume : nat} - -type storage = {bids : order list; asks : order list} - -type parameter = Buy of order | Sell of order - -let buy (order, s : order * storage) = ... -let sell (order, s : order * storage) = ... -let main (p, s : parameter * storage) = ... -``` - - - - -An attacker may notice some transaction, for example, a request to buy some big volume of asset. They may then _front-run_ this transaction and, anticipating the price going up, insert a _buy_ order at the current price before the trader's transaction. Thus, they can benefit from the price change by selling the asset at a higher price. - -In fact, if the front-runner is a baker, the so-called _miner extracted value_ [poses a big risk](https://arxiv.org/pdf/1904.05234.pdf) to security of blockchains in general. You should avoid letting miners get rewards from transaction ordering. In this particular case, moving the order book off-chain would be a good option. - -## Timestamps - -Aside from transaction ordering, bakers can manipulate other variables you might want to rely on. A classic example of such a value is `Tezos.get_now`. Previously, it used to be equal to the current block timestamp. This behaviour has been changed to eliminate straightforward manipulations. Since Tezos is a distributed system, there is no way to make sure the block was produced _exactly_ at the specified time. Thus, bakers could slightly adjust the timestamp to make a transaction produce a different result. - -In the current protocol, `Tezos.get_now` is equal to the _previous_ block timestamp plus a fixed value. Although `Tezos.get_now` becomes less manipulable with this new behaviour, the only assumption you can make is that the operation goes through _roughly about_ the specified timestamp. And, of course, you should never use `Tezos.get_now` as a source of randomness. - -## Reentrancy and call injection - -Tezos features a rather unconventional model of execution: -1. The contract state is updated _after_ the computations are completed. -2. The contracts cannot emit operations in the middle of execution. -3. Internal operations are _queued._ - -The first two points resemble the Checks-Effects-Interactions pattern popular in Solidity. In Ethereum, it is considered a best practice, and Tezos enforces this on the protocol level. Such restrictions help prevent reentrancy attacks: if the state of your contract is updated _before_ someone makes a reentrant call, this call would be treated as a regular one and should do no harm. - -Consider the following snippet in Solidity: -``` -function withdraw(uint256 amount) { - uint256 balance = balances[beneficiary]; - require(balance >= amount); - uint256 new_balance = balance - amount; - beneficiary.call.value(amount)(); - balances[beneficiary] = new_balance; -} -``` - -You may notice that the _effect_ of updating the storage happens after _interaction_ – transferring the `amount` to the beneficiary. This contract has a reentrancy vulnerability: the contract execution would get paused during the transfer, and the beneficiary can call `withdraw` again _before_ their balance is updated. - -It is quite hard to repeat this attack on Tezos, where the contract storage is always updated _before_ any interactions: - - - -```cameligo -type storage = {beneficiary : address; balances : (address, tez) map} - -type parameter = tez * (unit contract) - -let withdraw (param, s : parameter * storage) = - let @amount, beneficiary = param in - let beneficiary_addr = Tezos.address beneficiary in - let @balance = - match (Map.find_opt beneficiary_addr s.balances) with - Some v -> v - | None -> 0mutez in - let new_balance = match @balance - @amount with - | Some x -> x - | None -> (failwith "Insufficient balance" : tez) - in - let op = Tezos.Operation.transaction () @amount beneficiary in - let new_balances = - Map.update beneficiary_addr (Some new_balance) s.balances in - [op], {s with balances = new_balances} -``` - - - - -Notice that the code flow is similar: we first check whether the beneficiary has enough balance, then forge an operation that sends the money, and finally we update the balances mapping. The difference is that in Tezos the operations are not executed immediately: we store the operation and later return it as a result of the entrypoint. Hence, the balances are updated by the time the operation is executed, so the reentrancy attack is mitigated. - -However, in some cases reentrancy attacks are still possible, especially if contracts are supposed to "wait" for a callback in an indeterminate state. If you, for example, choose to store balances in a separate contract, your execution flow will need a lot more interactions than sending one internal operation: - -| Current call | Treasury state after | Queued operations | -|--------------|----------------------|-------------------| -| `Treasury %withdraw` | Waiting for balances | [`Balances %getBalance`] | -| `Balances %getBalance` | Waiting for balances | [`Treasury %withdrawContinuation`] | -| `Treasury %withdrawContinuation` | Sent | [Send tez to `Beneficiary`, `Balances %setNewBalance`] | -| Send tez to `Beneficiary` | Sent | [`Balances %setNewBalance`] | -| `Balances %setNewBalance` | Sent | | - -In this example, the Treasury contract uses a callback mechanism to get the sender balance. In an intermediate state between `%withdraw` and `%withdrawContinuation`, the balances request has already been sent but the funds have not been withdrawn yet, and the balances have not been updated. This opens up a possibility for a call injection attack. - -For example, here is what happens if an attacker tries to call `%withdraw` twice within a single transaction: - -| Step | Current call | Queued operations | -|------|--------------|-------------------| -| 1 | `Evil %attack` | [`Treasury %withdraw`, `Treasury %withdraw`] | -| 2 | `Treasury %withdraw` | [`Balances %getBalance`] | -| 3 | `Treasury %withdraw` | [`Balances %getBalance`, `Balances %getBalance`] | -| 4 | `Balances %getBalance`| [`Balances %getBalance`, `Treasury %withdrawContinuation`] | -| 5 | `Balances %getBalance`| [`Treasury %withdrawContinuation`, `Treasury %withdrawContinuation`] | -| 6 | `Treasury %withdrawContinuation` | [`Treasury %withdrawContinuation`, Send tez to `Beneficiary`, `Balances %setNewBalance`] | -| 7 | `Treasury %withdrawContinuation` | [Send tez to `Beneficiary`, `Balances %setNewBalance`, Send tez to `Beneficiary`, `Balances %setNewBalance`] | -| 8 | Send tez to `Beneficiary` | [`Balances %setNewBalance`, Send tez to `Beneficiary`, `Balances %setNewBalance`] | -| 9 | `Balances %setNewBalance` | [Send tez to `Beneficiary`, `Balances %setNewBalance`] | -| 10 | Send tez to `Beneficiary` | [`Balances %setNewBalance`] | -| 11 | `Balances %setNewBalance` | | - -The attacker successfully withdraws money twice using the fact that by the time the second `%withdraw` is called, the balance has not been updated yet. - -## Transactions to untrusted contracts - -When emitting a transaction to an untrusted contract, you can not assume that it will "play by the rules". Rather, you should always bear in mind that the callee may fail, causing the entire operation to fail, or emit other operations you do not expect. - -Let us consider the following example: - - - -```cameligo -type storage = {owner : address; beneficiaries : address list} - -let send_rewards (beneficiary_addr : address) = - let maybe_contract = - Tezos.get_contract_opt beneficiary_addr in - let beneficiary = - match maybe_contract with - Some contract -> contract - | None -> (failwith "CONTRACT_NOT_FOUND" : unit contract) in - Tezos.Operation.transaction () 5000000mutez beneficiary - -let main (p, s : unit * storage) = - if (Tezos.get_sender ()) <> s.owner - then (failwith "ACCESS_DENIED" : operation list * storage) - else - let ops = List.map send_rewards s.beneficiaries in - ops, s -``` - - - - -The contract emits a bunch of operations that transfer 5 tez to each of the beneficiaries listed in storage. The flaw here is that one of the receiver contracts may fail, preventing others from receiving the reward. This may be intentional censorship or a bug in the receiver contract – in either case, the contract gets stuck. - -Instead of making a batch transfer, it is better to let beneficiaries withdraw their funds individually. This way, if the receiver contract fails, it would not affect other withdrawals. - -## Incorrect authorisation checks - -When developing a contract, you may often want to restrict access to certain entrypoint. You need to somehow ensure that: -1. The request comes from an authorised entity -2. This entity cannot be tricked into sending this request. - -You may be tempted to use `Tezos.get_source` instruction – it returns the address of an implicit account who injected the operation – but this violates our second requirement. It is easy to ask the owner of this implicit account to make a seemingly innocent transfer to a malicious contract that, in turn, emits an operation to a restricted entrypoint. The attacker contract may disguise itself as some blockchain game or a DAO, but neither the caller would be aware of its side-effects nor the callee would notice the presence of the intermediary. You should **never** use `Tezos.get_source` for authorisation purposes. - -Checking whether `Tezos.get_sender` – the address of the immediate caller – is authorised to perform an operation is better: since the request comes directly from the authorised entity, we can be more certain this call is intended. Such an approach is a decent default choice if both conditions hold true: -1. The sender contract is well secured against emitting arbitrary operations. For instance, it must not contain ["view" entrypoints](https://gitlab.com/tzip/tzip/-/blob/master/proposals/tzip-4/tzip-4.md#view-entrypoints) as defined in [TZIP-4](https://gitlab.com/tzip/tzip/-/blob/master/proposals/tzip-4/tzip-4.md). -2. You only need to authorise an immediate caller and not the contracts somewhere up in the call chain. - -If any of these conditions is not met, you need to use a more advanced technique called "tickets". Tickets are much like "contract signatures": a contract may issue a ticket that authorises a certain action. A ticket holds the data of any type, and a number – ticket _amount_. A ticket can not be copied but it can be split. If you split a ticket of amount `N`, you would get two tickets with amounts `M` and `K` such that `N = M + K`. You can also join two tickets if they have the same data and are issued by the same contract. In this case, you would get a new ticket with the sum of the amounts. - -To check whether an action is authorised, you need to see if the ticket meets the following conditions: -1. The ticket issuer has enough permissions to perform this action. -2. The ticket amount and data are correct (the definition of "correct" is application-specific, e.g., the amount may mean the number of tokens to spend or the number of _times_ the action can be executed). - -We recommend using the sender-based authorisation only in simple scenarios, e.g., when the contract has a single "owner" contract controlled by an implicit account. Otherwise, it is better to use ticket-based authorisation. diff --git a/gitlab-pages/docs/tutorials/security/src/security/ungrouped.mligo b/gitlab-pages/docs/tutorials/security/src/security/ungrouped.mligo deleted file mode 100644 index 4cbbfcff2..000000000 --- a/gitlab-pages/docs/tutorials/security/src/security/ungrouped.mligo +++ /dev/null @@ -1,65 +0,0 @@ -type parameter = Fund | Send of address * tez - -type transaction = Incoming of address * tez | Outgoing of address * tez - -type storage = {owner : address; transactionLog : transaction list} - -type result = operation list * storage - -let do_send (dst, @amount : address * tez) = - let callee = Tezos.get_contract_opt dst in - match callee with - Some contract -> - let op = Tezos.Operation.transaction () @amount contract in - Outgoing (dst, @amount), [op] - | None -> (failwith "Could not send tokens" : transaction * operation list) - -let do_fund (from, @amount : address * tez) = - Incoming (from, @amount), ([] : operation list) - -[@entry] -let fund (_ : unit) (s : storage) : result = - let tx, ops = do_fund (Tezos.get_sender (), Tezos.get_amount ()) in - ops, { s with transactionLog = tx :: s.transactionLog } - -[@entry] -let send (args : address * tez) (s : storage) = - let u = Assert.assert ((Tezos.get_sender ()) = s.owner && (Tezos.get_amount ()) = 0mutez) in - let tx, ops = do_send args in - ops, { s with transactionLog = tx :: s.transactionLog } -type storage = {beneficiary : address; balances : (address, tez) map} - -type parameter = tez * (unit contract) - -let withdraw (param, s : parameter * storage) = - let @amount, beneficiary = param in - let beneficiary_addr = Tezos.address beneficiary in - let @balance = - match (Map.find_opt beneficiary_addr s.balances) with - Some v -> v - | None -> 0mutez in - let new_balance = match @balance - @amount with - | Some x -> x - | None -> (failwith "Insufficient balance" : tez) - in - let op = Tezos.Operation.transaction () @amount beneficiary in - let new_balances = - Map.update beneficiary_addr (Some new_balance) s.balances in - [op], {s with balances = new_balances} -type storage = {owner : address; beneficiaries : address list} - -let send_rewards (beneficiary_addr : address) = - let maybe_contract = - Tezos.get_contract_opt beneficiary_addr in - let beneficiary = - match maybe_contract with - Some contract -> contract - | None -> (failwith "CONTRACT_NOT_FOUND" : unit contract) in - Tezos.Operation.transaction () 5000000mutez beneficiary - -let main (p, s : unit * storage) = - if (Tezos.get_sender ()) <> s.owner - then (failwith "ACCESS_DENIED" : operation list * storage) - else - let ops = List.map send_rewards s.beneficiaries in - ops, s \ No newline at end of file diff --git a/gitlab-pages/website/sidebars.js b/gitlab-pages/website/sidebars.js index 6b3244ec5..596c7136b 100644 --- a/gitlab-pages/website/sidebars.js +++ b/gitlab-pages/website/sidebars.js @@ -137,7 +137,7 @@ const sidebars = { "Advanced Topics": [ "advanced/package-management", "tutorials/optimisation/optimisation", - "tutorials/security/security" + "advanced/security" ] }, "API": { From 40cd99386b4bd9aa4c812af2f27746ed36064c7e Mon Sep 17 00:00:00 2001 From: Tim McMackin Date: Wed, 16 Jul 2025 12:56:29 -0400 Subject: [PATCH 2/2] fix(docs): clarify what kind of views we mean here --- gitlab-pages/docs/advanced/security.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gitlab-pages/docs/advanced/security.md b/gitlab-pages/docs/advanced/security.md index 0495b9eb0..4d58463ef 100644 --- a/gitlab-pages/docs/advanced/security.md +++ b/gitlab-pages/docs/advanced/security.md @@ -458,7 +458,8 @@ Because the request comes directly from an authorised entity, contracts can be m This approach is a good default choice if both conditions hold true: 1. The sender contract is well secured against emitting arbitrary operations. -For instance, it must not contain ["view" entrypoints](https://gitlab.com/tzip/tzip/-/blob/master/proposals/tzip-4/tzip-4.md#view-entrypoints) as defined in [TZIP-4](https://gitlab.com/tzip/tzip/-/blob/master/proposals/tzip-4/tzip-4.md). +For instance, it must not contain a certain kind of ["view" entrypoints](https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-4/tzip-4.md#view-entrypoints#view-entrypoints) as defined in [TZIP-4](https://gitlab.com/tzip/tzip/-/blob/master/proposals/tzip-4/tzip-4.md). +Ordinary views with the `@view` attribute/decorator do not have this vulnerability because they cannot create operations. 2. You only need to authorise an immediate caller and not the contracts somewhere up in the call chain.