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