code4rena-2024-03-dittoeth-m03

[M-03] The colRedeemed variable is wrongly retrieved in LibBytes.readProposalData function

보고서

Summary

로우레벨로 데이터를 파싱할 때 잘못 파싱하여 잘못된 데이터를 이용한다.

Keyword

solidity assembly, parsing

Vulnerability

숏 청산 데이터는 가스비용 절약을 위해 새 컨트랙트의 바이트코드 대신 데이터를 넣고 컨트랙트를 배포하는 방식으로 저장된다. 청산 대상 숏 당 51 바이트의 데이터를 저장한다.

    function proposeRedemption(
        address asset,
        MTypes.ProposalInput[] calldata proposalInput,
        uint88 redemptionAmount,
        uint88 maxRedemptionFee
    ) external isNotFrozen(asset) nonReentrant {
        ...
 
        bytes memory slate;
        for (uint8 i = 0; i < proposalInput.length; i++) {
            ...
 
            // @dev directly write the properties of MTypes.ProposalData into bytes
            // instead of usual abi.encode to save on extra zeros being written
@>          slate = bytes.concat(
                slate,
                bytes20(p.shorter),
                bytes1(p.shortId),
                bytes8(uint64(p.currentCR)),
                bytes11(p.amountProposed),
                bytes11(p.colRedeemed)
            );
 
            LibSRUtil.disburseCollateral(p.asset, p.shorter, p.colRedeemed, currentSR.dethYieldRate, currentSR.updatedAt);
            p.redemptionCounter++;
            if (redemptionAmount - p.totalAmountProposed < minShortErc) break;
        }
 
        if (p.totalAmountProposed < minShortErc) revert Errors.RedemptionUnderMinShortErc();
 
        // @dev SSTORE2 the entire proposalData after validating proposalInput
@>      redeemerAssetUser.SSTORE2Pointer = SSTORE2.write(slate);
 
        ...
    }
 
    // SSTORE2.write
    function write(bytes memory data) internal returns (address pointer) {
        // Prefix the bytecode with a STOP opcode to ensure it cannot be called.
@>      bytes memory runtimeCode = abi.encodePacked(hex"00", data);
 
        bytes memory creationCode = abi.encodePacked(
            //---------------------------------------------------------------------------------------------------------------//
            // Opcode  | Opcode + Arguments  | Description  | Stack View                                                     //
            //---------------------------------------------------------------------------------------------------------------//
            // 0x60    |  0x600B             | PUSH1 11     | codeOffset                                                     //
            // 0x59    |  0x59               | MSIZE        | 0 codeOffset                                                   //
            // 0x81    |  0x81               | DUP2         | codeOffset 0 codeOffset                                        //
            // 0x38    |  0x38               | CODESIZE     | codeSize codeOffset 0 codeOffset                               //
            // 0x03    |  0x03               | SUB          | (codeSize - codeOffset) 0 codeOffset                           //
            // 0x80    |  0x80               | DUP          | (codeSize - codeOffset) (codeSize - codeOffset) 0 codeOffset   //
            // 0x92    |  0x92               | SWAP3        | codeOffset (codeSize - codeOffset) 0 (codeSize - codeOffset)   //
            // 0x59    |  0x59               | MSIZE        | 0 codeOffset (codeSize - codeOffset) 0 (codeSize - codeOffset) //
            // 0x39    |  0x39               | CODECOPY     | 0 (codeSize - codeOffset)                                      //
            // 0xf3    |  0xf3               | RETURN       |                                                                //
            //---------------------------------------------------------------------------------------------------------------//
            hex"60_0B_59_81_38_03_80_92_59_39_F3", // Returns all code in the contract except for the first 11 (0B in hex) bytes.
@>          runtimeCode // The bytecode we want the contract to have after deployment. Capped at 1 byte less than the code size limit.
        );
 
        /// @solidity memory-safe-assembly
        assembly {
            // Deploy a new contract with the generated creation code.
            // We start 32 bytes into the code to avoid copying the byte length.
@>          pointer := create(0, add(creationCode, 32), mload(creationCode))
        }
 
        require(pointer != address(0), "DEPLOYMENT_FAILED");
    }

데이터를 꺼내서 사용할 때는 컨트랙트의 바이트코드형태로 저장한 데이터를 읽고, 이를 로우레벨로 파싱한다. 그런데 파싱하는 로직 readProposalData 을 잘못 구현하여 데이터를 잘못 가져온다.

각 숏에 할당된 51바이트 중 마지막 11바이트는 colRedeemed 데이터를 의미한다. 이를 파싱하기 위해 29~51 인덱스의 바이트를 떼어온다. 상위 11바이트는 ercDebtRedeemed, 다음 11바이트는 colRedeemed 를 의미하고, 한 번에 32바이트를 로드하므로 나머지 하위 10 바이트는 버려야 한다.

colRedeemed := add(0xffffffffffffffffffffff, shr(80, fullWord)) 에서 로드한 32 바이트의 10바이트만큼을 shift 해서 옮기고 중간에 끼어있던 11바이트를 떼어내려 한다. 올바르게 떼어내려면 add 대신 and 를 해야 한다. 그런데 add 를 하여 잘못된 colRedeemed 를 얻고 있다.

    function readProposalData(address SSTORE2Pointer, uint8 slateLength) internal view returns (MTypes.ProposalData[] memory) {
        bytes memory slate = SSTORE2.read(SSTORE2Pointer);
        // ProposalData is 51 bytes
        require(slate.length % 51 == 0, "Invalid data length");
 
        MTypes.ProposalData[] memory data = new MTypes.ProposalData[](slateLength);
 
        for (uint256 i = 0; i < slateLength; i++) {
            // 32 offset for array length, mulitply by each ProposalData
            uint256 offset = i * 51 + 32;
 
            address shorter; // bytes20
            uint8 shortId; // bytes1
            uint64 CR; // bytes8
            uint88 ercDebtRedeemed; // bytes11
            uint88 colRedeemed; // bytes11
 
            assembly {
                // mload works 32 bytes at a time
                let fullWord := mload(add(slate, offset))
                // read 20 bytes
                shorter := shr(96, fullWord) // 0x60 = 96 (256-160)
                // read 8 bytes
                shortId := and(0xff, shr(88, fullWord)) // 0x58 = 88 (96-8), mask of bytes1 = 0xff * 1
                // read 64 bytes
                CR := and(0xffffffffffffffff, shr(24, fullWord)) // 0x18 = 24 (88-64), mask of bytes8 = 0xff * 8
 
                fullWord := mload(add(slate, add(offset, 29))) // (29 offset) // [29]~[51] 인덱스 바이트 로드
                // read 88 bytes
                ercDebtRedeemed := shr(168, fullWord) // (256-88 = 168)
                // read 88 bytes
@>              colRedeemed := add(0xffffffffffffffffffffff, shr(80, fullWord)) // (256-88-88 = 80), mask of bytes11 = 0xff * 11
            }
 
            data[i] = MTypes.ProposalData({
                shorter: shorter,
                shortId: shortId,
                CR: CR,
                ercDebtRedeemed: ercDebtRedeemed,
                colRedeemed: colRedeemed
            });
        }
 
        return data;
    }

Impact

데이터를 잘못 해석하여 claimRedemption, disputeRedemption, claimRemainingCollateral 함수가 오작동할 수 있다.

Mitigation

colRedeemed := and(0xffffffffffffffffffffff, shr(80, fullWord)) 로 and 로 마스킹한다.


tags: bughunting, dittoeth, smart contract, solidity, solidity assembly, severity medium