|
| 1 | +# External Function |
| 2 | + |
| 3 | +**Severity**: `Gas` |
| 4 | +**ID**: `external-function` |
| 5 | + |
| 6 | +`public` functions that are never called from inside the contract (or any of its |
| 7 | +derivatives) can be declared `external`. External functions read their reference-type |
| 8 | +arguments directly from `calldata` instead of copying them into `memory`, which saves |
| 9 | +gas at every call site. |
| 10 | + |
| 11 | +## What it does |
| 12 | + |
| 13 | +Flags a `public` function declaration when **all** of the following hold: |
| 14 | + |
| 15 | +- The function is `public` (not `external`, `internal`, or `private`). |
| 16 | +- It is an ordinary function (not a constructor, fallback, receive, or modifier). |
| 17 | +- It has at least one parameter that is a reference type (`struct`, array, `bytes`, or |
| 18 | + `string`) currently located in `memory`. |
| 19 | +- It is not an `override` of another function (the base must be migrated first). |
| 20 | +- It has a body (not abstract or interface-only). |
| 21 | +- It does not write to any of its parameters inside the body. |
| 22 | +- It is never called from inside the contract or any contract that derives from it, |
| 23 | + whether directly (`foo()`), via `super.foo(...)`, or via a function-pointer reference |
| 24 | + (`fn = foo;`). |
| 25 | + |
| 26 | +The lint runs in the `Gas` severity bucket and is automatically skipped on Foundry |
| 27 | +test and script files. |
| 28 | + |
| 29 | +## Why is this bad? |
| 30 | + |
| 31 | +Calling a `public` function from outside the contract is more expensive than calling |
| 32 | +the equivalent `external` function: |
| 33 | + |
| 34 | +- Each reference-type parameter is copied from `calldata` into `memory` before the |
| 35 | + function body executes, even though `external`-only callers never need that copy. |
| 36 | +- The opcode shim that allows the function to be called both internally and externally |
| 37 | + adds a few bytes of bytecode and an extra branch on every entry. |
| 38 | + |
| 39 | +When the function is never called internally, switching `public` to `external` removes |
| 40 | +both costs at no semantic change. |
| 41 | + |
| 42 | +## Example |
| 43 | + |
| 44 | +### Bad |
| 45 | + |
| 46 | +```solidity |
| 47 | +contract Vault { |
| 48 | + mapping(address => uint256) public balances; |
| 49 | +
|
| 50 | + function deposit(address[] memory accounts, uint256[] memory amounts) public { |
| 51 | + for (uint256 i = 0; i < accounts.length; i++) { |
| 52 | + balances[accounts[i]] += amounts[i]; |
| 53 | + } |
| 54 | + } |
| 55 | +} |
| 56 | +``` |
| 57 | + |
| 58 | +`deposit` is never called from inside `Vault`, but its `memory` arrays force an |
| 59 | +unnecessary calldata-to-memory copy on every external call. |
| 60 | + |
| 61 | +### Good |
| 62 | + |
| 63 | +```solidity |
| 64 | +contract Vault { |
| 65 | + mapping(address => uint256) public balances; |
| 66 | +
|
| 67 | + function deposit(address[] calldata accounts, uint256[] calldata amounts) external { |
| 68 | + for (uint256 i = 0; i < accounts.length; i++) { |
| 69 | + balances[accounts[i]] += amounts[i]; |
| 70 | + } |
| 71 | + } |
| 72 | +} |
| 73 | +``` |
| 74 | + |
| 75 | +When you migrate `public` to `external`, also change reference-type parameters from |
| 76 | +`memory` to `calldata` to capture the full gas saving. |
0 commit comments