Skip to content

Commit fb676e4

Browse files
committed
Merge with develop
2 parents 83ca28a + cd11d4c commit fb676e4

4 files changed

Lines changed: 40 additions & 22 deletions

File tree

.github/agents/agent-docs/BITE.md

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# BITE on SKALE — Auditor's Context
22

3-
This document is written for a Solidity audit agent reviewing contracts that import `@skalenetwork/bite-solidity`. It describes how a BITE-enabled SKALE chain differs from Ethereum, what Conditional Transactions (CTXs) are, and the invariants an auditor must keep in mind when reasoning about confidentiality, re-entrancy, gas accounting, and the callback trust boundary.
3+
<!-- cspell:words ciphertext ECIES -->
4+
5+
This document is written for a Solidity audit agent reviewing contracts that import `@skalenetwork/bite-solidity`. It describes how a BITE-enabled SKALE chain differs from Ethereum, what Conditional Transactions (CTXs) are, and the invariants an auditor must keep in mind when reasoning about confidentiality, reentrancy, gas accounting, and the callback trust boundary.
46

57
---
68

79
## 1. How SKALE (with BITE) differs from Ethereum
810

9-
SKALE chains are EVM-compatible, but several runtime assumptions that hold on Ethereum do not hold on SKALE — and some new ones are introduced by BITE. An auditor should keep these in mind when analysing any contract deployed to a BITE-enabled SKALE chain.
11+
SKALE chains are EVM-compatible, but several runtime assumptions that hold on Ethereum do not hold on SKALE — and some new ones are introduced by BITE. An auditor should keep these in mind when analyzing any contract deployed to a BITE-enabled SKALE chain.
1012

1113
### 1.1 Chain-level differences (SKALE, independent of BITE)
1214

@@ -22,7 +24,7 @@ SKALE chains are EVM-compatible, but several runtime assumptions that hold on Et
2224
- **Conditional Transactions (CTXs).** A contract can submit a transaction that will be executed by the chain itself after the BITE network threshold-decrypts payloads off-chain. The callback that delivers the decrypted data is a **system-originated transaction**, not a normal user tx. See §4.
2325
- **Encrypted storage.** The intended design is that sensitive data exists on-chain only as TE or ECIES ciphertext; decryption happens transiently during a CTX callback or off-chain if ECIES encrypted. `onDecrypt` execution trace is protected - only saved memory will be "revealed".
2426
- **Freshly generated callback senders.** Each CTX callback is delivered from a newly generated, CTX-unique EOA-like address (`ctxSender`). Trust of `msg.sender` inside `onDecrypt` is established by whitelisting this address at submission time — never by any other means.
25-
- **Asynchronous execution in separate transactions.** `submitCTX` returns synchronously, but `onDecrypt` fires in a *later* block. Any invariant that a reviewer would normally check with "this happens atomically within one tx" does NOT hold across a CTX boundary - should watchout close for Atomicity issues.
27+
- **Asynchronous execution in separate transactions.** `submitCTX` returns synchronously, but `onDecrypt` fires in a *later* block. Any invariant that a reviewer would normally check with "this happens atomically within one tx" does NOT hold across a CTX boundary - watch out closely for atomicity issues.
2628

2729
---
2830

@@ -58,7 +60,7 @@ This is the entry point the BITE network invokes from `ctxSender` in a later blo
5860

5961
### 3.1 `EncryptTE``0x1D` (staticcall, view-safe)
6062

61-
Encrypts arbitrary bytes under the **network public threshold key**. Only the BITE network (via a threshold of nodes) can decrypt. When an account submits the request, it's address is saved in the cyphertext. Only that account is allowed to schedule decryption of such values - otherwise anyone could write a contract to decrypt any value encrypted by another account.
63+
Encrypts arbitrary bytes under the **network public threshold key**. Only the BITE network (via a threshold of nodes) can decrypt. When an account submits the request, it's address is saved in the ciphertext. Only that account is allowed to schedule decryption of such values - otherwise anyone could write a contract to decrypt any value encrypted by another account.
6264

6365
### 3.2 `EncryptECIES``0x1C` (staticcall, view-safe)
6466

@@ -111,7 +113,7 @@ function requestReveal(bytes calldata someEncryptedInput) external payable {
111113
plaintextArgs
112114
);
113115
114-
_canCallOnDecrypt[ctxSender] = true; // authorise exactly this ctxSender
116+
_canCallOnDecrypt[ctxSender] = true; // authorize exactly this ctxSender
115117
ctxSender.sendValue(msg.value); // fund the callback
116118
}
117119
```
@@ -121,7 +123,7 @@ Step-by-step semantics during this transaction:
121123
1. `BITE.submitCTX` performs a low-level `call` to `0x1B` with ABI-encoded `(gasLimit, abi.encode(encryptedArgs, plaintextArgs))`.
122124
2. The precompile validates: shape, encoded offsets, TE-ciphertext sizes, destination (the calling contract), signature/transaction construction internals. Any failure reverts with a typed `CTX*` error from `SubmitCTXErrors`.
123125
3. On success, the precompile returns a 20-byte `ctxSender` address and emits `CTXSubmitted(ctxSender)` (from the library, not the precompile).
124-
4. The supplicant contract **must** record authorisation of this `ctxSender` before the transaction ends (typically `_canCallOnDecrypt[ctxSender] = true`). It **must** also transfer enough value to `ctxSender` to cover `gasLimit * tx.gasprice`.
126+
4. The supplicant contract **must** record authorization of this `ctxSender` before the transaction ends (typically `_canCallOnDecrypt[ctxSender] = true`). It **must** also transfer enough value to `ctxSender` to cover `gasLimit * tx.gasprice`.
125127
5. Transaction 1 ends. No decryption has happened yet. Nothing has been revealed. It is essential that state-changes remain atomic - if a transaction depends on a CTX, make all state changes during the `onDecrypt` callback.
126128

127129
### 4.2 Between transactions — BITE network work
@@ -131,7 +133,7 @@ The network observes the CTX, performs threshold decryption of each `encryptedAr
131133
This happens in a later block. The guarantees are as follows:
132134
- CTXs are scheduled for the **next block** (N+1).
133135
- Each block still verifies gasLimit, thus if for some reason the amount of CTXs scheduled for block N+1 is too much, the remaining CTXs are re-scheduled for block N+2, and so on. decrypted CTXs take precedence over regular transactions - when picking transactions for a block, first the block is filled with CTXs.
134-
- Order is guaranteed - CTXs are executed by the same order they are scheduled. However, between CTX submission and execution, other state changes can occurr by other transactions.
136+
- Order is guaranteed - CTXs are executed by the same order they are scheduled. However, between CTX submission and execution, other state changes can occur by other transactions.
135137

136138
### 4.3 Transaction 2 — Callback (`onDecrypt`)
137139

@@ -159,19 +161,19 @@ function onDecrypt(
159161
}
160162
```
161163

162-
NOTE: This pattern does not clear senders from failed transactions. This is considered safe because it is considered *impossible* to get the key for such address to re-sign a transaction. The state is changed to `false` on successfull ones to minimze used storage.
164+
NOTE: This pattern does not clear senders from failed transactions. This is considered safe because it is considered *impossible* to get the key for such address to re-sign a transaction. The state is changed to `false` on successful ones to minimize used storage.
163165

164166

165-
### 4.5 Re-entrancy and state consistency
167+
### 4.5 Reentrancy and state consistency
166168

167-
- `submitCTX` is a `call` to a system precompile. The precompile is trusted and performs no external calls back into user contracts. **Re-entrancy from the precompile itself is not possible.**
169+
- `submitCTX` is a `call` to a system precompile. The precompile is trusted and performs no external calls back into user contracts. **Reentrancy from the precompile itself is not possible.**
168170
- However, `onDecrypt` runs in a **later transaction** with arbitrary contract state evolved in between. Anything a normal tx can do (price changes, role changes, pauses, upgrades) can have happened between submission and callback. Treat the callback as a fresh, adversarially-scheduled tx with respect to every state variable that is not explicitly snapshotted into `plaintextArguments`, storage keyed on `ctxSender`, or the encrypted payload.
169-
- Multiple CTXs can be in flight simultaneously. `onDecrypt` may be invoked with interleavings unrelated to submission order. Per-CTX state should be keyed on `ctxSender`, not on globals.
171+
- Multiple CTXs can be in flight simultaneously. `onDecrypt` may be interleaved and is not guaranteed to match the submission order. Per-CTX state should be keyed on `ctxSender`, not on globals.
170172
- `onDecrypt` itself may call `submitCTX` (self-referential CTX chains). This means a callback can submit further CTXs whose callbacks will fire later. Audit for unbounded recursion / gas griefing and for correct termination conditions.
171173

172174
### 4.6 Gas accounting inside `onDecrypt`
173175

174-
The callback is executed with exactly `GAS_LIMIT` gas (the value passed to `submitCTX`). If `onDecrypt` runs out of gas, the callback reverts, and (depending on chain behaviour and the refund policy) the CTX may be dropped.
176+
The callback is executed with exactly `GAS_LIMIT` gas (the value passed to `submitCTX`). If `onDecrypt` runs out of gas, the callback reverts, and (depending on chain behavior and the refund policy) the CTX may be dropped.
175177

176178
### 4.7 What can go wrong with the plaintext once it is inside `onDecrypt`
177179

@@ -180,16 +182,16 @@ The plaintext exists in memory for the duration of the callback. Audit for:
180182
- Writing plaintext to storage (makes it world-readable forever).
181183
- Emitting plaintext in events (events are public).
182184
- Don't trust passing plaintext to other (arbitrary) contracts in non-view functions.
183-
- Verify re-encryption of said sensitive texts (usualy via ECIES), and what Public Key is used
185+
- Verify re-encryption of said sensitive texts (usually via ECIES), and what Public Key is used
184186

185187
A correctly-written supplicant either (a) re-encrypts the plaintext under ECIES for a specific viewer and stores the ECIES ciphertext, or (b) uses the plaintext to drive a single decision (e.g. "did this bidder offer >= reserve?") and discards it without persistence.
186188

187189
---
188190

189-
## 5. Consensus and block-rule behaviour of CTXs
191+
## 5. Consensus and block-rule behavior of CTXs
190192

191193
- SKALE networks have a fixed block limit.
192-
- CTXs, once scheduled, are saved to be executed in the first available block after the one they're scheduled in. They take priority over regular transactions, thus if there are pending CTXs for a given block, other transactions are put on hold untill a block has space for them.
194+
- CTXs, once scheduled, are saved to be executed in the first available block after the one they're scheduled in. They take priority over regular transactions, thus if there are pending CTXs for a given block, other transactions are put on hold until a block has space for them.
193195
- We can assume execution context (trace) of CTXs (onDecrypt) is hidden, only saved storage/events are revealed as usual.
194196
- CTXs are executed in the exact same order they are scheduled.
195197
- For each CTX, a random address is generated to be the sender of such CTX. This address is known upon CTX scheduling, and should be topped up with gas enough to pay for the CTX, otherwise it fails (CTX may not appear in the block)

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ The main contract implementing the confidential token functionality. It extends
4040
- Callback fee configuration for confidential operations (transfers and view key changes)
4141
- Supports depositing gas token to pay for confidential operations
4242
- EIP-3009 support for transfers with authorizations via signed messages.
43-
- ERC-2616 support for permits that change authorization amounts
43+
- ERC-2612 support for permits that change authorization amounts
4444
- Integration with SKALE's precompiled contracts and BITE protocol (Phase 2)
4545

4646
**Core Functions:**

contracts/ConfidentialToken.sol

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -537,12 +537,6 @@ contract ConfidentialToken is ConfidentialEIP3009, ERC20Permit, AccessManaged, I
537537
)
538538
internal
539539
{
540-
if (from == to) {
541-
_setBalance(from, fromBalance);
542-
_onUpdate(from, to, value);
543-
return;
544-
}
545-
546540
uint256 updatedFromBalance = fromBalance;
547541
uint256 updatedToBalance = toBalance;
548542

@@ -552,6 +546,11 @@ contract ConfidentialToken is ConfidentialEIP3009, ERC20Permit, AccessManaged, I
552546
if (fromBalance < value) {
553547
revert InsufficientBalance();
554548
}
549+
if (from == to) {
550+
_setBalance(from, fromBalance);
551+
_onUpdate(from, to, value);
552+
return;
553+
}
555554
updatedFromBalance = fromBalance - value;
556555
}
557556

test/ConfidentialToken.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,23 @@ describe("ConfidentialToken", () => {
266266
balanceAfter.should.be.equal(balanceBefore);
267267
});
268268

269+
it("should not allow to do self transfer if value exceeds balance", async () => {
270+
const [owner] = await ethers.getSigners();
271+
const { token, bite } = await withMintedTokens();
272+
273+
await token.connect(owner).setViewerPublicKey(
274+
await getPublicKey(owner)
275+
);
276+
await bite.sendCallback();
277+
278+
const balance = await balanceOf(token, bite, owner);
279+
280+
await token.connect(owner).transfer(owner, balance + 1n);
281+
await expect(
282+
bite.sendCallback()
283+
).to.be.revertedWithCustomError(token, "InsufficientBalance()");
284+
});
285+
269286
it("should always charge callback fee from the sender of transferFrom", async () => {
270287
const amount = ethers.parseEther("1.0");
271288
const [owner, spender ] = await ethers.getSigners();

0 commit comments

Comments
 (0)