The resolveQueuedTrades
function present in the BufferRouter
contract keeps track of the next queue indices to be processed (both at the user and global level) and may be wrongly calculated if items in the queue aren't processed in order.
The BufferRouter
contract keeps track of which items are next to be processed in the queue using the storage variables userNextQueueIndexToProcess
(to track it at the user level) and nextQueueIdToProcess
(to track the global index next to be processed).
These values are updated in the resolveQueuedTrades
when the keeper processes the queued trades. The userNextQueueIndexToProcess
variable is updated by taking the userQueueIndex
param (which is the length of the user queued items at the time it was initiated) and adding 1 to it:
userNextQueueIndexToProcess[queuedTrade.user] =
queuedTrade.userQueueIndex +
1;
The nextQueueIdToProcess
is also updated in a similar way, it takes the queueId
from the last element of the OpenTradeParams[]
array parameter and it adds 1 to it:
nextQueueIdToProcess = params[params.length - 1].queueId + 1;
Both of these calculations assume that elements are being processed in order, and will end up with inconsistent values if not.
This will potentially lead to inconsistent values if queued trades are not processed strictly in order, and eventually leave items unprocessed.
If trade A is opened before trade B, meaning trade A queue id will be lower than trade B queue id, and trade B gets resolved first, then nextQueueIdToProcess
will be trade B queue id plus 1, while trade A queue id (which is strictly lower) is still unprocessed.
If keepers use the nextQueueIdToProcess
value to keep track of upcoming trades to be processed, then this can lead to some trades being missed and ignored.
The following test reproduces the issue. Here, Alice opens a trade first with queueId == 0
, then Bob opens another trade with queueId == 1
. The keeper resolves Bob's trade, which ends up leaving nextQueueIdToProcess
with a value of 2, while Alice's trade is still in queue.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.4;
import "forge-std/Test.sol";
import "../src/core/BufferRouter.sol";
import "../src/core/BufferBinaryOptions.sol";
import "../src/core/BufferBinaryPool.sol";
import "../src/core/OptionsConfig.sol";
import "../src/core/ReferralStorage.sol";
import "../src/interfaces/Interfaces.sol";
import "../src/tokens/USDC.sol";
import "../src/tokens/BadERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract AuditPoolTest is Test {
bytes32 constant ROUTER_ROLE = keccak256("ROUTER_ROLE");
bytes32 constant OPTION_ISSUER_ROLE = keccak256("OPTION_ISSUER_ROLE");
event OpenTrade(address indexed account, uint256 queueId, uint256 optionId);
address owner;
uint256 publisherPrivateKey;
address publisher;
address feeRecipient;
address keeper;
address alice;
address bob;
function setUp() public {
owner = makeAddr("owner");
publisherPrivateKey = 0x1234;
publisher = vm.addr(publisherPrivateKey);
feeRecipient = makeAddr("feeRecipient");
keeper = makeAddr("keeper");
alice = makeAddr("alice");
bob = makeAddr("bob");
}
function test_BuffeRouter_resolveQueuedTrades_IncorrectNextQueueIds() public {
// Setup
vm.startPrank(owner);
USDC token = new USDC();
BufferBinaryPool pool = new BufferBinaryPool(token);
OptionsConfig config = new OptionsConfig(pool);
ReferralStorage referral = new ReferralStorage();
BufferBinaryOptions binary = new BufferBinaryOptions(
token,
pool,
config,
referral,
IBufferBinaryOptions.AssetCategory.Crypto,
"ETH/USDC"
);
BufferRouter router = new BufferRouter(publisher);
config.setSettlementFeeDisbursalContract(feeRecipient);
router.setContractRegistry(address(binary), true);
router.setKeeper(keeper, true);
binary.grantRole(ROUTER_ROLE, address(router));
binary.approvePoolToTransferTokenX();
pool.grantRole(OPTION_ISSUER_ROLE, address(binary));
// ensure pool has tokens
token.approve(address(pool), type(uint256).max);
pool.provide(10e6, 0);
vm.stopPrank();
// END Setup
// Provide Alice and Bob with tokens
uint256 amount = 1e6;
vm.prank(owner);
token.transfer(alice, amount);
vm.prank(owner);
token.transfer(bob, amount);
// Alice creates a trade, this is the first trade so it's queueId should be 0
vm.prank(alice);
token.approve(address(router), type(uint256).max);
vm.prank(alice);
uint256 aliceQueueId = router.initiateTrade(
amount, // uint256 totalFee,
1 hours, // uint256 period,
true, // bool isAbove,
address(binary), // address targetContract,
1250, // uint256 expectedStrike,
5e2,// uint256 slippage,
true, // bool allowPartialFill,
"", // string memory referralCode,
0 // uint256 traderNFTId
);
assertEq(aliceQueueId, 0);
// Next, Bob creates a trade, queueId should be 1
vm.prank(bob);
token.approve(address(router), type(uint256).max);
vm.prank(bob);
uint256 bobQueueId = router.initiateTrade(
amount, // uint256 totalFee,
1 hours, // uint256 period,
false, // bool isAbove,
address(binary), // address targetContract,
1250, // uint256 expectedStrike,
5e2,// uint256 slippage,
true, // bool allowPartialFill,
"", // string memory referralCode,
0 // uint256 traderNFTId
);
assertEq(bobQueueId, 1);
// The keeper resolves Bob's trade first
address asset = address(binary);
uint256 timestamp = block.timestamp;
uint256 price = 1250;
bytes32 digest = ECDSA.toEthSignedMessageHash(
keccak256(abi.encodePacked(timestamp, asset, price))
);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(publisherPrivateKey, digest);
IBufferRouter.OpenTradeParams[] memory params = new IBufferRouter.OpenTradeParams[](1);
params[0].queueId = bobQueueId;
params[0].timestamp = timestamp;
params[0].asset = asset;
params[0].price = price;
params[0].signature = abi.encodePacked(r,s,v);
vm.prank(keeper);
router.resolveQueuedTrades(params);
// This will update nextQueueIdToProcess to 2, while Alice's trade is still queued
assertEq(router.nextQueueIdToProcess(), 2);
}
}
None
This isn't a blocking issue since a keeper can still process older trades (i.e. trades with lower queue id than next to be processed). However, consider a) removing these variables since they aren't used in the contract and may lead to reporting inconsistent values or b) enforce a correct order so the values are accurate.