diff --git a/src/utils/Base58.sol b/src/utils/Base58.sol index a6f6d391f..e3af96294 100644 --- a/src/utils/Base58.sol +++ b/src/utils/Base58.sol @@ -189,10 +189,17 @@ library Base58 { for { let j := 0 } 1 {} { let c := sub(byte(0, mload(add(s, j))), 49) + // Check if the input character is valid before `mload(c)`. + // Otherwise an out-of-bounds `c` expands memory and reverts + // with out-of-gas instead of `Base58DecodingError`. + if iszero(and(shl(c, 1), 0x3fff7ff03ffbeff01ff)) { + mstore(0x00, 0xe8fad793) // `Base58DecodingError()`. + revert(0x1c, 0x04) + } let p := mul(result, 58) let acc := add(byte(0, mload(c)), p) - // Check if the input character is valid. - if iszero(and(0x3fff7ff03ffbeff01ff, shl(c, lt(lt(acc, p), lt(result, t))))) { + // Check for multiplication or addition overflow. + if iszero(lt(lt(acc, p), lt(result, t))) { mstore(0x00, 0xe8fad793) // `Base58DecodingError()`. revert(0x1c, 0x04) } diff --git a/test/Base58.t.sol b/test/Base58.t.sol index f39dfbe6e..83c392d45 100644 --- a/test/Base58.t.sol +++ b/test/Base58.t.sol @@ -259,6 +259,21 @@ contract Base58Test is SoladyTest { this.decodeWord("JEKNVnkbo3jma5nREBBJCDoXFVeKkD56V3xKrvRmWxFH@"); } + function testDecodeWordLowCharacterReverts() public { + // Characters below '1' (0x31) underflow the lookup index. The + // sanitizer must run before `mload(c)`, otherwise the out-of-bounds + // load expands memory and reverts with out-of-gas instead of a clean + // `Base58DecodingError`. See https://github.com/Vectorized/solady/issues/1543. + vm.expectRevert(Base58.Base58DecodingError.selector); + this.decodeWord("0"); + vm.expectRevert(Base58.Base58DecodingError.selector); + this.decodeWord("\x00"); + // Also cover an underflowing byte after a valid character, where + // `result` is already nonzero (loop position > 0). + vm.expectRevert(Base58.Base58DecodingError.selector); + this.decodeWord("z0"); + } + function decodeWord(string memory encoded) public pure returns (bytes32) { return Base58.decodeWord(encoded); }