diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 762a296..3fb5e52 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,8 +38,3 @@ jobs: run: | forge build --sizes id: build - - - name: Run Forge tests - run: | - forge test -vvv - id: test diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4f3500a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "solidity.packageDefaultDependenciesContractsDirectory": "src", + "solidity.packageDefaultDependenciesDirectory": "lib" +} \ No newline at end of file diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..83fbb69 --- /dev/null +++ b/remappings.txt @@ -0,0 +1 @@ +forge-std/=lib/forge-std/src/ \ No newline at end of file diff --git a/script/Counter.s.sol b/script/Counter.s.sol deleted file mode 100644 index cdc1fe9..0000000 --- a/script/Counter.s.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console} from "forge-std/Script.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterScript is Script { - Counter public counter; - - function setUp() public {} - - function run() public { - vm.startBroadcast(); - - counter = new Counter(); - - vm.stopBroadcast(); - } -} diff --git a/src/AddressStringUtil.sol b/src/AddressStringUtil.sol new file mode 100644 index 0000000..abb5fbe --- /dev/null +++ b/src/AddressStringUtil.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +library AddressStringUtil { + // converts an address to the uppercase hex string, extracting only len bytes (up to 20, multiple of 2) + function toAsciiString(address addr, uint256 len) internal pure returns (string memory) { + require(len % 2 == 0 && len > 0 && len <= 40, "AddressStringUtil: INVALID_LEN"); + + bytes memory s = new bytes(len); + uint256 addrNum = uint256(uint160(addr)); + for (uint256 i = 0; i < len / 2; i++) { + // shift right and truncate all but the least significant byte to extract the byte at position 19-i + uint8 b = uint8(addrNum >> (8 * (19 - i))); + // first hex character is the most significant 4 bits + uint8 hi = b >> 4; + // second hex character is the least significant 4 bits + uint8 lo = b - (hi << 4); + s[2 * i] = char(hi); + s[2 * i + 1] = char(lo); + } + return string(s); + } + + // hi and lo are only 4 bits and between 0 and 16 + // this method converts those values to the unicode/ascii code point for the hex representation + // uses upper case for the characters + function char(uint8 b) private pure returns (bytes1 c) { + if (b < 10) { + return bytes1(b + 0x30); + } else { + return bytes1(b + 0x37); + } + } +} diff --git a/src/Babylonian.sol b/src/Babylonian.sol new file mode 100644 index 0000000..4a6ad0a --- /dev/null +++ b/src/Babylonian.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +// computes square roots using the babylonian method +// https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method +library Babylonian { + // credit for this implementation goes to + // https://github.com/abdk-consulting/abdk-libraries-solidity/blob/master/ABDKMath64x64.sol#L687 + function sqrt(uint256 x) internal pure returns (uint256) { + if (x == 0) return 0; + // this block is equivalent to r = uint256(1) << (BitMath.mostSignificantBit(x) / 2); + // however that code costs significantly more gas + uint256 xx = x; + uint256 r = 1; + if (xx >= 0x100000000000000000000000000000000) { + xx >>= 128; + r <<= 64; + } + if (xx >= 0x10000000000000000) { + xx >>= 64; + r <<= 32; + } + if (xx >= 0x100000000) { + xx >>= 32; + r <<= 16; + } + if (xx >= 0x10000) { + xx >>= 16; + r <<= 8; + } + if (xx >= 0x100) { + xx >>= 8; + r <<= 4; + } + if (xx >= 0x10) { + xx >>= 4; + r <<= 2; + } + if (xx >= 0x8) { + r <<= 1; + } + r = (r + x / r) >> 1; + r = (r + x / r) >> 1; + r = (r + x / r) >> 1; + r = (r + x / r) >> 1; + r = (r + x / r) >> 1; + r = (r + x / r) >> 1; + r = (r + x / r) >> 1; // Seven iterations should be enough + uint256 r1 = x / r; + return (r < r1 ? r : r1); + } +} diff --git a/src/BitMath.sol b/src/BitMath.sol new file mode 100644 index 0000000..d5f4ff1 --- /dev/null +++ b/src/BitMath.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +library BitMath { + // returns the 0 indexed position of the most significant bit of the input x + // s.t. x >= 2**msb and x < 2**(msb+1) + function mostSignificantBit(uint256 x) internal pure returns (uint8 r) { + require(x > 0, "BitMath::mostSignificantBit: zero"); + + if (x >= 0x100000000000000000000000000000000) { + x >>= 128; + r += 128; + } + if (x >= 0x10000000000000000) { + x >>= 64; + r += 64; + } + if (x >= 0x100000000) { + x >>= 32; + r += 32; + } + if (x >= 0x10000) { + x >>= 16; + r += 16; + } + if (x >= 0x100) { + x >>= 8; + r += 8; + } + if (x >= 0x10) { + x >>= 4; + r += 4; + } + if (x >= 0x4) { + x >>= 2; + r += 2; + } + if (x >= 0x2) r += 1; + } + + // returns the 0 indexed position of the least significant bit of the input x + // s.t. (x & 2**lsb) != 0 and (x & (2**(lsb) - 1)) == 0) + // i.e. the bit at the index is set and the mask of all lower bits is 0 + function leastSignificantBit(uint256 x) internal pure returns (uint8 r) { + require(x > 0, "BitMath::leastSignificantBit: zero"); + + r = 255; + if (x & type(uint128).max > 0) { + r -= 128; + } else { + x >>= 128; + } + if (x & type(uint64).max > 0) { + r -= 64; + } else { + x >>= 64; + } + if (x & type(uint32).max > 0) { + r -= 32; + } else { + x >>= 32; + } + if (x & type(uint16).max > 0) { + r -= 16; + } else { + x >>= 16; + } + if (x & type(uint8).max > 0) { + r -= 8; + } else { + x >>= 8; + } + if (x & 0xf > 0) { + r -= 4; + } else { + x >>= 4; + } + if (x & 0x3 > 0) { + r -= 2; + } else { + x >>= 2; + } + if (x & 0x1 > 0) r -= 1; + } +} diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/FixedPoint.sol b/src/FixedPoint.sol new file mode 100644 index 0000000..81c26b4 --- /dev/null +++ b/src/FixedPoint.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +import "./FullMath.sol"; +import "./Babylonian.sol"; +import "./BitMath.sol"; + +// a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format)) +library FixedPoint { + // range: [0, 2**112 - 1] + // resolution: 1 / 2**112 + struct uq112x112 { + uint224 _x; + } + + // range: [0, 2**144 - 1] + // resolution: 1 / 2**112 + struct uq144x112 { + uint256 _x; + } + + uint8 public constant RESOLUTION = 112; + uint256 public constant Q112 = 0x10000000000000000000000000000; // 2**112 + uint256 private constant Q224 = 0x100000000000000000000000000000000000000000000000000000000; // 2**224 + uint256 private constant LOWER_MASK = 0xffffffffffffffffffffffffffff; // decimal of UQ*x112 (lower 112 bits) + + // encode a uint112 as a UQ112x112 + function encode(uint112 x) internal pure returns (uq112x112 memory) { + return uq112x112(uint224(x) << RESOLUTION); + } + + // encodes a uint144 as a UQ144x112 + function encode144(uint144 x) internal pure returns (uq144x112 memory) { + return uq144x112(uint256(x) << RESOLUTION); + } + + // decode a UQ112x112 into a uint112 by truncating after the radix point + function decode(uq112x112 memory self) internal pure returns (uint112) { + return uint112(self._x >> RESOLUTION); + } + + // decode a UQ144x112 into a uint144 by truncating after the radix point + function decode144(uq144x112 memory self) internal pure returns (uint144) { + return uint144(self._x >> RESOLUTION); + } + + // multiply a UQ112x112 by a uint, returning a UQ144x112 + // reverts on overflow + function mul(uq112x112 memory self, uint256 y) internal pure returns (uq144x112 memory) { + uint256 z = 0; + require(y == 0 || (z = self._x * y) / y == self._x, "FixedPoint::mul: overflow"); + return uq144x112(z); + } + + // multiply a UQ112x112 by an int and decode, returning an int + // reverts on overflow + function muli(uq112x112 memory self, int256 y) internal pure returns (int256) { + uint256 z = FullMath.mulDiv(self._x, uint256(y < 0 ? -y : y), Q112); + require(z < 2 ** 255, "FixedPoint::muli: overflow"); + return y < 0 ? -int256(z) : int256(z); + } + + // multiply a UQ112x112 by a UQ112x112, returning a UQ112x112 + // lossy + function muluq(uq112x112 memory self, uq112x112 memory other) internal pure returns (uq112x112 memory) { + if (self._x == 0 || other._x == 0) { + return uq112x112(0); + } + uint112 upper_self = uint112(self._x >> RESOLUTION); // * 2^0 + uint112 lower_self = uint112(self._x & LOWER_MASK); // * 2^-112 + uint112 upper_other = uint112(other._x >> RESOLUTION); // * 2^0 + uint112 lower_other = uint112(other._x & LOWER_MASK); // * 2^-112 + + // partial products + uint224 upper = uint224(upper_self) * upper_other; // * 2^0 + uint224 lower = uint224(lower_self) * lower_other; // * 2^-224 + uint224 uppers_lowero = uint224(upper_self) * lower_other; // * 2^-112 + uint224 uppero_lowers = uint224(upper_other) * lower_self; // * 2^-112 + + // so the bit shift does not overflow + require(upper <= type(uint112).max, "FixedPoint::muluq: upper overflow"); + + // this cannot exceed 256 bits, all values are 224 bits + uint256 sum = uint256(upper << RESOLUTION) + uppers_lowero + uppero_lowers + (lower >> RESOLUTION); + + // so the cast does not overflow + require(sum <= type(uint224).max, "FixedPoint::muluq: sum overflow"); + + return uq112x112(uint224(sum)); + } + + // divide a UQ112x112 by a UQ112x112, returning a UQ112x112 + function divuq(uq112x112 memory self, uq112x112 memory other) internal pure returns (uq112x112 memory) { + require(other._x > 0, "FixedPoint::divuq: division by zero"); + if (self._x == other._x) { + return uq112x112(uint224(Q112)); + } + if (self._x <= type(uint144).max) { + uint256 value = (uint256(self._x) << RESOLUTION) / other._x; + require(value <= type(uint224).max, "FixedPoint::divuq: overflow"); + return uq112x112(uint224(value)); + } + + uint256 result = FullMath.mulDiv(Q112, self._x, other._x); + require(result <= type(uint224).max, "FixedPoint::divuq: overflow"); + return uq112x112(uint224(result)); + } + + // returns a UQ112x112 which represents the ratio of the numerator to the denominator + // can be lossy + function fraction(uint256 numerator, uint256 denominator) internal pure returns (uq112x112 memory) { + require(denominator > 0, "FixedPoint::fraction: division by zero"); + if (numerator == 0) return FixedPoint.uq112x112(0); + + if (numerator <= type(uint144).max) { + uint256 result = (numerator << RESOLUTION) / denominator; + require(result <= type(uint224).max, "FixedPoint::fraction: overflow"); + return uq112x112(uint224(result)); + } else { + uint256 result = FullMath.mulDiv(numerator, Q112, denominator); + require(result <= type(uint224).max, "FixedPoint::fraction: overflow"); + return uq112x112(uint224(result)); + } + } + + // take the reciprocal of a UQ112x112 + // reverts on overflow + // lossy + function reciprocal(uq112x112 memory self) internal pure returns (uq112x112 memory) { + require(self._x != 0, "FixedPoint::reciprocal: reciprocal of zero"); + require(self._x != 1, "FixedPoint::reciprocal: overflow"); + return uq112x112(uint224(Q224 / self._x)); + } + + // square root of a UQ112x112 + // lossy between 0/1 and 40 bits + function sqrt(uq112x112 memory self) internal pure returns (uq112x112 memory) { + if (self._x <= type(uint144).max) { + return uq112x112(uint224(Babylonian.sqrt(uint256(self._x) << 112))); + } + + uint8 safeShiftBits = 255 - BitMath.mostSignificantBit(self._x); + safeShiftBits -= safeShiftBits % 2; + return uq112x112(uint224(Babylonian.sqrt(uint256(self._x) << safeShiftBits) << ((112 - safeShiftBits) / 2))); + } +} diff --git a/src/FullMath.sol b/src/FullMath.sol new file mode 100644 index 0000000..b1b8b1e --- /dev/null +++ b/src/FullMath.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: CC-BY-4.0 +pragma solidity ^0.8.26; + +// taken from https://medium.com/coinmonks/math-in-solidity-part-3-percents-and-proportions-4db014e080b1 +// license is CC-BY-4.0 +library FullMath { + function fullMul(uint256 x, uint256 y) internal pure returns (uint256 l, uint256 h) { + uint256 mm = mulmod(x, y, type(uint256).max); + l = x * y; + h = mm - l; + if (mm < l) h -= 1; + } + + function fullDiv(uint256 l, uint256 h, uint256 d) private pure returns (uint256) { + uint256 pow2 = d & (~d + 1); // equivalent to d & -d for uint256 + d /= pow2; + l /= pow2; + l += h * (uint256(-int256(pow2)) / pow2 + 1); + uint256 r = 1; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + return l * r; + } + + function mulDiv(uint256 x, uint256 y, uint256 d) internal pure returns (uint256) { + (uint256 l, uint256 h) = fullMul(x, y); + + uint256 mm = mulmod(x, y, d); + if (mm > l) h -= 1; + l -= mm; + + if (h == 0) return l / d; + + require(h < d, "FullMath: FULLDIV_OVERFLOW"); + return fullDiv(l, h, d); + } +} diff --git a/src/SafeERC20Namer.sol b/src/SafeERC20Namer.sol new file mode 100644 index 0000000..2ceee3d --- /dev/null +++ b/src/SafeERC20Namer.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +import "./AddressStringUtil.sol"; + +// produces token descriptors from inconsistent or absent ERC20 symbol implementations that can return string or bytes32 +// this library will always produce a string symbol to represent the token +library SafeERC20Namer { + function bytes32ToString(bytes32 x) private pure returns (string memory) { + bytes memory bytesString = new bytes(32); + uint256 charCount = 0; + for (uint256 j = 0; j < 32; j++) { + bytes1 char = x[j]; + if (char != 0) { + bytesString[charCount] = char; + charCount++; + } + } + bytes memory bytesStringTrimmed = new bytes(charCount); + for (uint256 j = 0; j < charCount; j++) { + bytesStringTrimmed[j] = bytesString[j]; + } + return string(bytesStringTrimmed); + } + + // assumes the data is in position 2 + function parseStringData(bytes memory b) private pure returns (string memory) { + uint256 charCount = 0; + // first parse the charCount out of the data + for (uint256 i = 32; i < 64; i++) { + charCount <<= 8; + charCount += uint8(b[i]); + } + + bytes memory bytesStringTrimmed = new bytes(charCount); + for (uint256 i = 0; i < charCount; i++) { + bytesStringTrimmed[i] = b[i + 64]; + } + + return string(bytesStringTrimmed); + } + + // uses a heuristic to produce a token name from the address + // the heuristic returns the full hex of the address string in upper case + function addressToName(address token) private pure returns (string memory) { + return AddressStringUtil.toAsciiString(token, 40); + } + + // uses a heuristic to produce a token symbol from the address + // the heuristic returns the first 6 hex of the address string in upper case + function addressToSymbol(address token) private pure returns (string memory) { + return AddressStringUtil.toAsciiString(token, 6); + } + + // calls an external view token contract method that returns a symbol or name, and parses the output into a string + function callAndParseStringReturn(address token, bytes4 selector) private view returns (string memory) { + (bool success, bytes memory data) = token.staticcall(abi.encodeWithSelector(selector)); + // if not implemented, or returns empty data, return empty string + if (!success || data.length == 0) { + return ""; + } + // bytes32 data always has length 32 + if (data.length == 32) { + bytes32 decoded = abi.decode(data, (bytes32)); + return bytes32ToString(decoded); + } else if (data.length > 64) { + return abi.decode(data, (string)); + } + return ""; + } + + // attempts to extract the token symbol. if it does not implement symbol, returns a symbol derived from the address + function tokenSymbol(address token) internal view returns (string memory) { + // 0x95d89b41 = bytes4(keccak256("symbol()")) + string memory symbol = callAndParseStringReturn(token, 0x95d89b41); + if (bytes(symbol).length == 0) { + // fallback to 6 uppercase hex of address + return addressToSymbol(token); + } + return symbol; + } + + // attempts to extract the token name. if it does not implement name, returns a name derived from the address + function tokenName(address token) internal view returns (string memory) { + // 0x06fdde03 = bytes4(keccak256("name()")) + string memory name = callAndParseStringReturn(token, 0x06fdde03); + if (bytes(name).length == 0) { + // fallback to full hex of address + return addressToName(token); + } + return name; + } +} diff --git a/src/TransferHelper.sol b/src/TransferHelper.sol new file mode 100644 index 0000000..7a5dd96 --- /dev/null +++ b/src/TransferHelper.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false +library TransferHelper { + function safeApprove(address token, address to, uint256 value) internal { + // bytes4(keccak256(bytes('approve(address,uint256)'))); + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value)); + require( + success && (data.length == 0 || abi.decode(data, (bool))), "TransferHelper::safeApprove: approve failed" + ); + } + + function safeTransfer(address token, address to, uint256 value) internal { + // bytes4(keccak256(bytes('transfer(address,uint256)'))); + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value)); + require( + success && (data.length == 0 || abi.decode(data, (bool))), "TransferHelper::safeTransfer: transfer failed" + ); + } + + function safeTransferFrom(address token, address from, address to, uint256 value) internal { + // bytes4(keccak256(bytes('transferFrom(address,address,uint256)'))); + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value)); + require( + success && (data.length == 0 || abi.decode(data, (bool))), + "TransferHelper::transferFrom: transferFrom failed" + ); + } + + function safeTransferETH(address to, uint256 value) internal { + (bool success,) = to.call{value: value}(new bytes(0)); + require(success, "TransferHelper::safeTransferETH: ETH transfer failed"); + } +} diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index 54b724f..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -}