Skip to content

Commit 15c39af

Browse files
Sam-Jestonfrankisawesomenaveen-imtb
authored
[TD-1397] ImmutableSignedZoneV2 internal audit fixes (#214)
* update readme Signed-off-by: Frank Li <[email protected]> * pin solidity version Signed-off-by: Frank Li <[email protected]> * perform check before state change Signed-off-by: Frank Li <[email protected]> * pin ImmutableSignedZoneV2 contracts to 0.8.20 * Revert if context is empty for ImmutableSignedZoneV2 * Revert "pin ImmutableSignedZoneV2 contracts to 0.8.20" This reverts commit 8da6123. * Restore pin * Disallow revoking role if the role member count is currently 1 * remove trailing comma * documentation rework * fix test Signed-off-by: Frank Li <[email protected]> * fix regression Signed-off-by: Frank Li <[email protected]> * update test name Signed-off-by: Frank Li <[email protected]> * add audit file Signed-off-by: Frank Li <[email protected]> * Update contracts/trading/seaport/zones/immutable-signed-zone/v2/README.md --------- Signed-off-by: Frank Li <[email protected]> Co-authored-by: Frank Li <[email protected]> Co-authored-by: Naveen <[email protected]>
1 parent d45c52d commit 15c39af

File tree

6 files changed

+140
-21
lines changed

6 files changed

+140
-21
lines changed
Binary file not shown.

contracts/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2.sol

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: Apache-2
33

44
// solhint-disable-next-line compiler-version
5-
pragma solidity ^0.8.20;
5+
pragma solidity 0.8.20;
66

77
import {AccessControlEnumerable} from "openzeppelin-contracts-5.0.2/access/extensions/AccessControlEnumerable.sol";
88
import {ECDSA} from "openzeppelin-contracts-5.0.2/utils/cryptography/ECDSA.sol";
@@ -22,6 +22,10 @@ import {SIP7Interface} from "./interfaces/SIP7Interface.sol";
2222
* @notice ImmutableSignedZoneV2 is a zone implementation based on the
2323
* SIP-7 standard https://github.com/ProjectOpenSea/SIPs/blob/main/SIPS/sip-7.md
2424
* implementing substandards 3, 4 and 6.
25+
*
26+
* The contract is not upgradable. If the contract needs to be changed a new version
27+
* should be deployed, and the old version should be removed from the Seaport contract
28+
* zone allowlist.
2529
*/
2630
contract ImmutableSignedZoneV2 is
2731
ERC165,
@@ -397,10 +401,14 @@ contract ImmutableSignedZoneV2 is
397401
uint256 startIndex = 0;
398402
uint256 contextLength = context.length;
399403

404+
// The ImmutableSignedZoneV2 contract enforces at least
405+
// one of the supported substandards is present in the context.
406+
if (contextLength == 0) {
407+
revert InvalidExtraData("invalid context, no substandards present", zoneParameters.orderHash);
408+
}
409+
400410
// Each _validateSubstandard* function returns the length of the substandard
401411
// segment (0 if the substandard was not matched).
402-
403-
if (startIndex == contextLength) return;
404412
startIndex = _validateSubstandard3(context[startIndex:], zoneParameters) + startIndex;
405413

406414
if (startIndex == contextLength) return;

contracts/trading/seaport/zones/immutable-signed-zone/v2/README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Contract threat models and audits:
1313

1414
| Description | Date | Version Audited | Link to Report |
1515
| ------------------------------- | ---- | --------------- | -------------- |
16-
| Not audited and no threat model | - | - | - |
16+
| Not audited and no threat model | 2024-05-02 | V2 | - ../../../audits/trading/202405-internal-audit-immutable-signed-zone-v2.pdf |
1717

1818
## ImmutableSignedZoneV2
1919

@@ -46,4 +46,18 @@ The sequence of events is as follows:
4646
2. The client calls `fulfillAdvancedOrder` or `fulfillAvailableAdavancedOrders` on `ImmutableSeaport.sol` to fulfill an order
4747
3. `ImmutableSeaport.sol` executes the fufilment by transferring items between parties
4848
4. `ImmutableSeaport.sol` calls `validateOrder` on `ImmutableSignedZoneV2.sol`, passing it the fulfilment execution details as well as the `extraData` parameter
49-
1. `ImmutableSignedZoneV2.sol` validates the fulfilment execution details using the `extraData` payload, reverting if expectations are not met
49+
5. `ImmutableSignedZoneV2.sol` validates the fulfilment execution details using the `extraData` payload, reverting if expectations are not met
50+
51+
## Differences compared to ImmutableSignedZone (v1)
52+
53+
The contract was developed based on ImmutableSignedZone, with the addition of:
54+
- SIP7 substandard 6 support
55+
- Role based access control to be role based
56+
57+
### ZoneAccessControl
58+
59+
The contract now uses a finer grained access control with role based access with the `ZoneAccessControl` interface, rather than the `Ownable` interface in the v1 contract. A seperate `zoneManager` roles is used to manage signers and an admin role used to control roles.
60+
61+
### Support of SIP7 substandard 6
62+
63+
The V2 contract now supports substandard-6 of the SIP7 specification, found here (https://github.com/immutable/platform-services/pull/12775). A server side signed order can adhere to substandard 3 + 4 (full fulfillment only) or substandard 6 + 4 (full or partial fulfillment).

contracts/trading/seaport/zones/immutable-signed-zone/v2/ZoneAccessControl.sol

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: Apache-2
33

44
// solhint-disable-next-line compiler-version
5-
pragma solidity ^0.8.20;
5+
pragma solidity 0.8.20;
66

77
import {AccessControl} from "openzeppelin-contracts-5.0.2/access/AccessControl.sol";
88
import {IAccessControl} from "openzeppelin-contracts-5.0.2/access/IAccessControl.sol";
@@ -31,21 +31,21 @@ abstract contract ZoneAccessControl is AccessControlEnumerable, ZoneAccessContro
3131
* @inheritdoc AccessControl
3232
*/
3333
function revokeRole(bytes32 role, address account) public override(AccessControl, IAccessControl) onlyRole(getRoleAdmin(role)) {
34-
super.revokeRole(role, account);
35-
36-
if (role == DEFAULT_ADMIN_ROLE && super.getRoleMemberCount(DEFAULT_ADMIN_ROLE) == 0) {
34+
if (role == DEFAULT_ADMIN_ROLE && super.getRoleMemberCount(DEFAULT_ADMIN_ROLE) == 1) {
3735
revert LastDefaultAdminRole(account);
3836
}
37+
38+
super.revokeRole(role, account);
3939
}
4040

4141
/**
4242
* @inheritdoc AccessControl
4343
*/
4444
function renounceRole(bytes32 role, address callerConfirmation) public override(AccessControl, IAccessControl) {
45-
super.renounceRole(role, callerConfirmation);
46-
47-
if (role == DEFAULT_ADMIN_ROLE && super.getRoleMemberCount(DEFAULT_ADMIN_ROLE) == 0) {
45+
if (role == DEFAULT_ADMIN_ROLE && super.getRoleMemberCount(DEFAULT_ADMIN_ROLE) == 1) {
4846
revert LastDefaultAdminRole(callerConfirmation);
4947
}
48+
49+
super.renounceRole(role, callerConfirmation);
5050
}
5151
}

test/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2.t.sol

Lines changed: 103 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -532,20 +532,45 @@ contract ImmutableSignedZoneV2Test is
532532

533533
function test_validateOrder_revertsIfSignerIsNotActive() public {
534534
ImmutableSignedZoneV2Harness zone = _newZoneHarness(OWNER);
535+
// no signer added
536+
535537
bytes32 orderHash = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9);
536538
uint64 expiration = 100;
537539

538-
bytes memory extraData =
539-
_buildExtraData(zone, SIGNER_PRIVATE_KEY, FULFILLER, expiration, orderHash, new bytes(0));
540+
SpentItem[] memory spentItems = new SpentItem[](1);
541+
spentItems[0] = SpentItem({itemType: ItemType.ERC1155, token: address(0x5), identifier: 222, amount: 10});
542+
543+
ReceivedItem[] memory receivedItems = new ReceivedItem[](1);
544+
ReceivedItem memory receivedItem = ReceivedItem({
545+
itemType: ItemType.ERC20,
546+
token: address(0x4),
547+
identifier: 0,
548+
amount: 20,
549+
recipient: payable(address(0x3))
550+
});
551+
receivedItems[0] = receivedItem;
552+
553+
bytes32[] memory orderHashes = new bytes32[](1);
554+
orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9);
555+
556+
// console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 1, 1));
557+
bytes32 substandard3Data = bytes32(0xec07a42041c18889c5c5dcd348923ea9f3d0979735bd8b3b687ebda38d9b6a31);
558+
bytes memory substandard4Data = abi.encode(orderHashes);
559+
bytes memory substandard6Data = abi.encodePacked(uint256(10), substandard3Data);
560+
bytes memory context = abi.encodePacked(
561+
bytes1(0x03), substandard3Data, bytes1(0x04), substandard4Data, bytes1(0x06), substandard6Data
562+
);
563+
564+
bytes memory extraData = _buildExtraData(zone, SIGNER_PRIVATE_KEY, FULFILLER, expiration, orderHash, context);
540565

541566
ZoneParameters memory zoneParameters = ZoneParameters({
542567
orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9),
543568
fulfiller: FULFILLER,
544569
offerer: OFFERER,
545-
offer: new SpentItem[](0),
546-
consideration: new ReceivedItem[](0),
570+
offer: spentItems,
571+
consideration: receivedItems,
547572
extraData: extraData,
548-
orderHashes: new bytes32[](0),
573+
orderHashes: orderHashes,
549574
startTime: 0,
550575
endTime: 0,
551576
zoneHash: bytes32(0)
@@ -556,6 +581,54 @@ contract ImmutableSignedZoneV2Test is
556581
zone.validateOrder(zoneParameters);
557582
}
558583

584+
function test_validateOrder_revertsIfContextIsEmpty() public {
585+
ImmutableSignedZoneV2Harness zone = _newZoneHarness(OWNER);
586+
bytes32 managerRole = zone.ZONE_MANAGER_ROLE();
587+
vm.prank(OWNER);
588+
zone.grantRole(managerRole, OWNER);
589+
vm.prank(OWNER);
590+
zone.addSigner(SIGNER);
591+
592+
bytes32 orderHash = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9);
593+
uint64 expiration = 100;
594+
595+
SpentItem[] memory spentItems = new SpentItem[](1);
596+
spentItems[0] = SpentItem({itemType: ItemType.ERC1155, token: address(0x5), identifier: 222, amount: 10});
597+
598+
ReceivedItem[] memory receivedItems = new ReceivedItem[](1);
599+
ReceivedItem memory receivedItem = ReceivedItem({
600+
itemType: ItemType.ERC20,
601+
token: address(0x4),
602+
identifier: 0,
603+
amount: 20,
604+
recipient: payable(address(0x3))
605+
});
606+
receivedItems[0] = receivedItem;
607+
608+
bytes32[] memory orderHashes = new bytes32[](1);
609+
orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9);
610+
611+
bytes memory extraData = _buildExtraDataWithoutContext(zone, SIGNER_PRIVATE_KEY, FULFILLER, expiration, orderHash, new bytes(0));
612+
613+
ZoneParameters memory zoneParameters = ZoneParameters({
614+
orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9),
615+
fulfiller: FULFILLER,
616+
offerer: OFFERER,
617+
offer: spentItems,
618+
consideration: receivedItems,
619+
extraData: extraData,
620+
orderHashes: orderHashes,
621+
startTime: 0,
622+
endTime: 0,
623+
zoneHash: bytes32(0)
624+
});
625+
626+
vm.expectRevert(
627+
abi.encodeWithSelector(InvalidExtraData.selector, "invalid context, no substandards present", zoneParameters.orderHash)
628+
);
629+
zone.validateOrder(zoneParameters);
630+
}
631+
559632
function test_validateOrder_returnsMagicValueOnSuccessfulValidation() public {
560633
ImmutableSignedZoneV2Harness zone = _newZoneHarness(OWNER);
561634
bytes32 managerRole = zone.ZONE_MANAGER_ROLE();
@@ -671,8 +744,7 @@ contract ImmutableSignedZoneV2Test is
671744
}
672745

673746
/* _validateSubstandards */
674-
675-
function test_validateSubstandards_emptyContext() public {
747+
function test_validateSubstandards_revertsIfEmptyContext() public {
676748
ImmutableSignedZoneV2Harness zone = _newZoneHarness(OWNER);
677749

678750
ZoneParameters memory zoneParameters = ZoneParameters({
@@ -688,6 +760,12 @@ contract ImmutableSignedZoneV2Test is
688760
zoneHash: bytes32(0)
689761
});
690762

763+
vm.expectRevert(
764+
abi.encodeWithSelector(
765+
InvalidExtraData.selector, "invalid context, no substandards present", zoneParameters.orderHash
766+
)
767+
);
768+
691769
zone.exposed_validateSubstandards(new bytes(0), zoneParameters);
692770
}
693771

@@ -1435,6 +1513,24 @@ contract ImmutableSignedZoneV2Test is
14351513
);
14361514
return extraData;
14371515
}
1516+
1517+
function _buildExtraDataWithoutContext(
1518+
ImmutableSignedZoneV2Harness zone,
1519+
uint256 signerPrivateKey,
1520+
address fulfiller,
1521+
uint64 expiration,
1522+
bytes32 orderHash,
1523+
bytes memory context
1524+
) private view returns (bytes memory) {
1525+
bytes32 eip712SignedOrderHash = zone.exposed_deriveSignedOrderHash(fulfiller, expiration, orderHash, context);
1526+
bytes memory extraData = abi.encodePacked(
1527+
bytes1(0),
1528+
fulfiller,
1529+
expiration,
1530+
_signCompact(signerPrivateKey, ECDSA.toTypedDataHash(zone.exposed_domainSeparator(), eip712SignedOrderHash))
1531+
);
1532+
return extraData;
1533+
}
14381534
}
14391535

14401536
// solhint-enable func-name-mixedcase

test/trading/seaport/zones/immutable-signed-zone/v2/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Operational function tests:
5050
| `test_validateOrder_revertsIfActualFulfillerDoesNotMatchExpectedFulfiller` | Validate order with unexpected fufiller. | No | Yes |
5151
| `test_validateOrder_revertsIfActualFulfillerDoesNotMatchExpectedFulfiller` | Validate order with expected *any* fufiller. | Yes | No |
5252
| `test_validateOrder_revertsIfSignerIsNotActive` | Validate order with inactive signer. | No | Yes |
53+
| `test_validateOrder_revertsIfContextIsEmpty` | Validate order with an empty context. | No | Yes |
5354
| `test_validateOrder_returnsMagicValueOnSuccessfulValidation` | Validate order successfully. | Yes | Yes |
5455

5556
Internal operational function tests:
@@ -61,7 +62,7 @@ Internal operational function tests:
6162
| `test_deriveDomainSeparator_returnsDomainSeparatorForChainID` | Domain separator derivation. | Yes | Yes |
6263
| `test_getSupportedSubstandards` | Retrieve Zone's supported substandards. | Yes | Yes |
6364
| `test_deriveSignedOrderHash_returnsHashOfSignedOrder` | Signed order hash derivation. | Yes | Yes |
64-
| `test_validateSubstandards_emptyContext` | Empty context without substandards. | Yes | Yes |
65+
| `test_validateSubstandards_revertsIfEmptyContext` | Empty context without substandards should revert. | No | Yes |
6566
| `test_validateSubstandards_substandard3` | Context with substandard 3. | Yes | Yes |
6667
| `test_validateSubstandards_substandard4` | Context with substandard 4. | Yes | Yes |
6768
| `test_validateSubstandards_substandard6` | Context with substandard 6. | Yes | Yes |
@@ -98,4 +99,4 @@ All of these tests are in [test/trading/seaport/ImmutableSeaportSignedZoneV2Inte
9899
| `test_fulfillAdvancedOrder_withCompleteFulfilment` | Full fulfilment. | Yes | Yes |
99100
| `test_fulfillAdvancedOrder_withPartialFill` | Partial fulfilment. | Yes | Yes |
100101
| `test_fulfillAdvancedOrder_withMultiplePartialFills` | Sequential partial fulfilments. | Yes | Yes |
101-
| `test_fulfillAdvancedOrder_withOverfilling` | Over fulfilment. | Yes | Yes |
102+
| `test_fulfillAdvancedOrder_withOverfilling` | Over fulfilment. | Yes | Yes |

0 commit comments

Comments
 (0)