Skip to content
This repository has been archived by the owner on Jan 13, 2023. It is now read-only.

Security Review Limit Order Contracts #303

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- On old LimitOrder / SignedOperationProxy should disable operation and set owner to 0 before going live
3 changes: 3 additions & 0 deletions __tests__/proxies/SignedOperationProxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,9 @@ describe('SignedOperationProxy', () => {
]);
await expectInvalid([signedWithdrawOperation, signedOperation2]);
});

// Maybe also test:
// - authorization that overflows the actions arrays
});
});

Expand Down
2 changes: 2 additions & 0 deletions contracts/external/lib/TypedSignature.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ library TypedSignature {

// ============ Enums ============

// Put some commets as to what these types are
enum SignatureType {
NoPrepend,
Decimal,
Expand Down Expand Up @@ -82,6 +83,7 @@ library TypedSignature {
uint8 rawSigType;

/* solium-disable-next-line security/no-inline-assembly */
// Can we use abi.decode here?
assembly {
r := mload(add(signatureWithType, 0x20))
s := mload(add(signatureWithType, 0x40))
Expand Down
15 changes: 13 additions & 2 deletions contracts/external/proxies/SignedOperationProxy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ contract SignedOperationProxy is
// EIP712 encodeType of Action
bytes constant private EIP712_ACTION_STRING = abi.encodePacked(
"Action(",
"uint8 actionType,",
"uint8 actionType,", // Is it ok that this is uint8?
"address accountOwner,",
"uint256 accountNumber,",
"uint256 accountNumber,", // We call this accountId elsewhere
"AssetAmount assetAmount,",
"uint256 primaryMarketId,",
"uint256 secondaryMarketId,",
Expand Down Expand Up @@ -233,6 +233,7 @@ contract SignedOperationProxy is
)
public
{
// Don't think it matters functionally, but maybe also require auth.numActions == actions.length
bytes32 operationHash = getOperationHash(
accounts,
actions,
Expand Down Expand Up @@ -296,10 +297,13 @@ contract SignedOperationProxy is
);

// consider the signer to be msg.sender unless there is a signature
// This is weird because validateAccountOwner already special cases msg.sender
// Probably just set it to 0
address signer = msg.sender;

// if there is a signature, then validate it
if (auth.signature.length != 0) {
// Split this out into a helper function
// get the hash of the operation
bytes32 operationHash = getOperationHash(
accounts,
Expand Down Expand Up @@ -333,6 +337,8 @@ contract SignedOperationProxy is

// cache the index of the first action after this auth
uint256 actionEndIdx = actionStartIdx.add(auth.numActions);
// may be better to explicitly require actionEndIdx < actions.length
// rather than relying on solidity out of bounds checks

// loop over all actions for which this auth applies
for (uint256 actionIdx = actionStartIdx; actionIdx < actionEndIdx; actionIdx++) {
Expand Down Expand Up @@ -397,6 +403,9 @@ contract SignedOperationProxy is
view
{
bool valid =
// THOUGHT: Wouldn't signer now be set to msg.sender if no signature, so this is unneeded?
// answer: this is still needed because you could have an action block that touches
// both signer and msg.sender accounts
msg.sender == accountOwner
|| signer == accountOwner
|| SOLO_MARGIN.getIsLocalOperator(accountOwner, msg.sender)
Expand Down Expand Up @@ -466,9 +475,11 @@ contract SignedOperationProxy is

// for each action that corresponds to the auth
for (uint256 i = 0; i < auth.numActions; i++) {
// what happens if auth.numActions > actions.length? Maybe we should just require this
Actions.ActionArgs memory action = actions[startIdx + i];

// if action type has no second account, assume null account
// what happens if action.otherAccountId > accounts.length? Maybe we should just require this
Account.Info memory otherAccount =
(Actions.getAccountLayout(action.actionType) == Actions.AccountLayout.OnePrimary)
? Account.Info({ owner: address(0), number: 0 })
Expand Down
5 changes: 5 additions & 0 deletions contracts/external/traders/LimitOrders.sol
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ contract LimitOrders is

// verify taker
Require.that(
// Line over max-len, does linter not catch this?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess not, will fix

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Investigated and it seems that solium has a bug where it doesn't identify max-len in some cases (if the long line is not the first line of a statement)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(orderInfo.order.takerAccountOwner == address(0) && orderInfo.order.takerAccountNumber == 0 ) ||
(orderInfo.order.takerAccountOwner == takerAccount.owner && orderInfo.order.takerAccountNumber == takerAccount.number),
FILE,
Expand Down Expand Up @@ -564,6 +565,7 @@ contract LimitOrders is
return totalMakerFilledAmount;
}

// Nit: order these helper functions in the order they are used

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fix

/**
* Parses the order, verifies that it is not expired or canceled, and verifies the signature.
*/
Expand All @@ -575,6 +577,7 @@ contract LimitOrders is
returns (OrderInfo memory)
{
Require.that(
// Should validate it is either exactly NUM_ORDER_BYTES or NUM_ORDER_BYTES + NUM_SIGNATURE_BYTES

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do this, but abi.decode uses the same logic (it doesn't care if the bytes array is too long). Thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm it seems like in general we should be as specific as possible

data.length >= NUM_ORDER_BYTES,
FILE,
"Cannot parse order from data"
Expand Down Expand Up @@ -625,6 +628,7 @@ contract LimitOrders is
/* solium-disable-next-line indentation */
bytes32 structHash = keccak256(abi.encode(
EIP712_LIMIT_ORDER_STRUCT_SCHEMA_HASH,
// Does abi encoding structs just lay out all fields side by side with no padding?
order
));

Expand All @@ -648,6 +652,7 @@ contract LimitOrders is
returns (bytes memory)
{
Require.that(
// Why not exactly NUM_ORDER_BYTES + NUM_SIGNATURE_BYTES?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do this, but abi.decode uses the same logic (it doesn't care if the bytes array is too long). Thoughts?

data.length >= NUM_ORDER_BYTES + NUM_SIGNATURE_BYTES,
FILE,
"Cannot parse signature from data"
Expand Down
4 changes: 3 additions & 1 deletion contracts/protocol/interfaces/ICallee.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ contract ICallee {
* @param data Arbitrary data given by the sender
*/
function callFunction(
address sender,
address sender, // This is set to msg.sender, so this would be the auth proxy for everything...

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah nothing uses it (yet?) but it's already baked-in to the interface

// None of our contracts even use this right now though
// Same could be said of the payable proxy
Account.Info memory accountInfo,
bytes memory data
)
Expand Down