diff --git a/script/deploy_VestingTwap.s.sol b/script/deploy_VestingTwap.s.sol new file mode 100644 index 0000000..3b8f671 --- /dev/null +++ b/script/deploy_VestingTwap.s.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {Script} from "forge-std/Script.sol"; + +import {ComposableCoW} from "../src/ComposableCoW.sol"; + +import {VestingTWAP} from "../src/types/twap/VestingTWAP.sol"; +import {VestingContextFactory} from "../src/value_factories/VestingContextFactory.sol"; + +contract DeployVestingTwap is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address composableCow = vm.envAddress("COMPOSABLE_COW"); + vm.startBroadcast(deployerPrivateKey); + + new VestingTWAP(ComposableCoW(composableCow)); + new VestingContextFactory(); + + vm.stopBroadcast(); + } +} diff --git a/src/interfaces/IVestingEscrow.sol b/src/interfaces/IVestingEscrow.sol new file mode 100644 index 0000000..3317c55 --- /dev/null +++ b/src/interfaces/IVestingEscrow.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; +import {IERC20} from "../BaseConditionalOrder.sol"; + +/** + * @title TBD + * @dev TBD + */ +interface IVestingEscrow { + function token() external view returns (IERC20); + function openClaim() external view returns (bool); + function unclaimed() external view returns (uint256); + function locked() external view returns (uint256); + function recipient() external view returns (address); + function endTime() external view returns (uint256); + function startTime() external view returns (uint256); + function totalLocked() external view returns (uint256); + function cliffLength() external view returns (uint256); +} diff --git a/src/types/twap/VestingTWAP.sol b/src/types/twap/VestingTWAP.sol new file mode 100644 index 0000000..f65ff55 --- /dev/null +++ b/src/types/twap/VestingTWAP.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {ComposableCoW} from "../../ComposableCoW.sol"; + +import {IConditionalOrder, IConditionalOrderGenerator, GPv2Order, BaseConditionalOrder, IERC20} from "../../BaseConditionalOrder.sol"; +import "./libraries/TWAPOrder.sol"; +import {IVestingEscrow} from "../../interfaces/IVestingEscrow.sol"; +import {VestingContextEncoder} from "./libraries/VestingContextEncoder.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {VestingMathLib} from "./libraries/VestingMathLib.sol"; + +// --- error strings + +string constant NOT_WITHIN_SPAN = "not within span"; +string constant VESTING_END = "vesting end"; +string constant LOW_FIRST_BATCH = "low first batch"; +string constant INVALID_CLAIM_AMOUNT = "invalid claim amount"; + +contract VestingTWAP is BaseConditionalOrder { + using SafeCast for uint256; + + ComposableCoW public immutable composableCow; + + constructor(ComposableCoW _composableCow) { + composableCow = _composableCow; + } + + struct Data { + IERC20 buyToken; + address receiver; + IVestingEscrow vesting; + uint256 claimAmount; + bytes32 appDataTwap; // should containg claim hook + bytes32 appDataFirstOrder; // shouldn't contain claim hook + uint256 minPartLimit; + uint256 span; + uint256 minFirstPartLimit; + } + + function getTradeableOrder( + address owner, + address, + bytes32 ctx, + bytes calldata staticInput, + bytes calldata + ) public view override returns (GPv2Order.Data memory order) { + Data memory data = abi.decode(staticInput, (Data)); + + ( + uint256 orderCreationTime, + uint256 initialClaimAmount + ) = VestingContextEncoder.decode(composableCow.cabinet(owner, ctx)); + + uint256 endTime = data.vesting.endTime(); + + if (orderCreationTime > endTime) { + revert IConditionalOrder.OrderNotValid(VESTING_END); + } + + uint256 initialVestingLocked = VestingMathLib.lockedAt( + orderCreationTime, + data.vesting.startTime(), + endTime, + data.vesting.totalLocked(), + data.vesting.cliffLength() + ); + + if ( + !VestingMathLib.verifyClaimAmount( + data.claimAmount, + initialVestingLocked + ) + ) { + revert IConditionalOrder.OrderNotValid(INVALID_CLAIM_AMOUNT); + } + + uint256 period = VestingMathLib.calculatePeriod( + data.claimAmount, + endTime, + orderCreationTime, + initialVestingLocked + ); + + uint256 firstBatchValidFrom = orderCreationTime + period; + + if (block.timestamp > firstBatchValidFrom) { + order = _twapOrder( + data, + firstBatchValidFrom, + period, + initialVestingLocked + ); + } else { + order = _firstOrder(data, firstBatchValidFrom, initialClaimAmount); + } + + if (!(block.timestamp <= order.validTo)) { + revert IConditionalOrder.OrderNotValid(NOT_WITHIN_SPAN); + } + } + + function _twapOrder( + Data memory data, + uint256 firstBatchValidFrom, + uint256 period, + uint256 initialVestingLocked + ) internal view returns (GPv2Order.Data memory order) { + uint256 n = VestingMathLib.calculateBatchLenght( + data.claimAmount, + initialVestingLocked + ); + + TWAPOrder.Data memory twap = TWAPOrder.Data({ + sellToken: data.vesting.token(), + buyToken: data.buyToken, + receiver: data.receiver, + partSellAmount: data.claimAmount, + minPartLimit: data.minPartLimit, + t0: firstBatchValidFrom, + n: n, + t: period, + span: data.span, + appData: data.appDataTwap + }); + + order = TWAPOrder.orderFor(twap); + } + + function _firstOrder( + Data memory data, + uint256 firstBatchValidFrom, + uint256 initialClaimAmount + ) internal view returns (GPv2Order.Data memory order) { + IERC20 sellToken = data.vesting.token(); + + if (data.buyToken == sellToken) { + revert IConditionalOrder.OrderNotValid(INVALID_SAME_TOKEN); + } + + if ( + !(address(data.buyToken) != address(0) && + address(data.buyToken) != address(0)) + ) { + revert IConditionalOrder.OrderNotValid(INVALID_TOKEN); + } + + if (firstBatchValidFrom > type(uint32).max) { + revert IConditionalOrder.OrderNotValid(INVALID_START_TIME); + } + + if (initialClaimAmount < data.claimAmount) { + revert IConditionalOrder.PollTryAtEpoch( + firstBatchValidFrom, + LOW_FIRST_BATCH + ); + } + + order = GPv2Order.Data({ + sellToken: sellToken, + buyToken: data.buyToken, + receiver: data.receiver, + sellAmount: initialClaimAmount, + buyAmount: data.minFirstPartLimit, + validTo: (firstBatchValidFrom + data.span).toUint32(), + appData: data.appDataFirstOrder, + feeAmount: 0, + kind: GPv2Order.KIND_SELL, + partiallyFillable: false, + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20 + }); + } +} diff --git a/src/types/twap/libraries/VestingContextEncoder.sol b/src/types/twap/libraries/VestingContextEncoder.sol new file mode 100644 index 0000000..ea77b05 --- /dev/null +++ b/src/types/twap/libraries/VestingContextEncoder.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +string constant TIMESTAMP_TOO_LARGE = "Timestamp too large"; +string constant VALUE_TOO_LARGE = "Value too large"; + +library VestingContextEncoder { + uint256 private constant _TIMESTAMP_BITS = 40; + uint256 private constant _VALUE_BITS = 216; // Increased to use all remaining bits + + uint256 private constant _TIMESTAMP_MASK = (1 << _TIMESTAMP_BITS) - 1; + uint256 private constant _VALUE_MASK = (1 << _VALUE_BITS) - 1; + + function encode( + uint256 timestamp, + uint256 value + ) public pure returns (bytes32) { + require(timestamp <= _TIMESTAMP_MASK, TIMESTAMP_TOO_LARGE); + require(value <= _VALUE_MASK, VALUE_TOO_LARGE); + + return bytes32((value << _TIMESTAMP_BITS) | timestamp); + } + + function decode( + bytes32 encoded + ) public pure returns (uint256 timestamp, uint256 value) { + uint256 decoded = uint256(encoded); + + timestamp = decoded & _TIMESTAMP_MASK; + value = (decoded >> _TIMESTAMP_BITS) & _VALUE_MASK; + } +} diff --git a/src/types/twap/libraries/VestingMathLib.sol b/src/types/twap/libraries/VestingMathLib.sol new file mode 100644 index 0000000..24c78b8 --- /dev/null +++ b/src/types/twap/libraries/VestingMathLib.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol"; + +library VestingMathLib { + using Math for uint256; + using SafeMath for uint256; + + function lockedAt( + uint256 time, + uint256 startTime, + uint256 endTime, + uint256 totalLocked, + uint256 cliffLength + ) public pure returns (uint256) { + if (time <= startTime + cliffLength) { + return totalLocked; + } + if (time >= endTime) { + return 0; + } + + return totalLocked.mulDiv(endTime.sub(time), endTime.sub(startTime)); + } + + function calculatePeriod( + uint256 claimAmount, + uint256 endTime, + uint256 orderCreationTime, + uint256 initialVestingLocked + ) public pure returns (uint256) { + if (claimAmount > initialVestingLocked) { + return 0; + } + return + claimAmount.mulDiv( + endTime.sub(orderCreationTime), + initialVestingLocked + ); + } + + function calculateBatchLenght( + uint256 claimAmount, + uint256 initialVestingLocked + ) public pure returns (uint256) { + return initialVestingLocked.div(claimAmount); + } + + function verifyClaimAmount( + uint256 claimAmount, + uint256 initialVestingLocked + ) public pure returns (bool) { + return claimAmount > 0 && claimAmount.div(2) < initialVestingLocked; + } +} diff --git a/src/value_factories/VestingContextFactory.sol b/src/value_factories/VestingContextFactory.sol new file mode 100644 index 0000000..4d3764c --- /dev/null +++ b/src/value_factories/VestingContextFactory.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import {IVestingEscrow} from "../interfaces/IVestingEscrow.sol"; +import {VestingContextEncoder} from "../types/twap/libraries/VestingContextEncoder.sol"; + +contract VestingContextFactory { + uint256 private constant _TIMESTAMP_BITS = 40; + uint256 private constant _VALUE_BITS = 104; + + uint256 private constant _TIMESTAMP_MASK = (1 << _TIMESTAMP_BITS) - 1; + uint256 private constant _VALUE_MASK = (1 << _VALUE_BITS) - 1; + + function getValue(bytes memory data) public view returns (bytes32) { + IVestingEscrow vestingContract = IVestingEscrow(bytesToAddress(data)); + return + VestingContextEncoder.encode( + block.timestamp, + vestingContract.unclaimed() + ); + } + + function bytesToAddress(bytes memory _bytes) public pure returns (address) { + require(_bytes.length == 20, "Invalid address length"); + address addr; + assembly { + addr := mload(add(_bytes, 20)) + } + return addr; + } +} diff --git a/test/ComposableCow.vestingtwap.t.sol b/test/ComposableCow.vestingtwap.t.sol new file mode 100644 index 0000000..cdcf093 --- /dev/null +++ b/test/ComposableCow.vestingtwap.t.sol @@ -0,0 +1,393 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {Test} from "forge-std/Test.sol"; +import "../src/types/twap/VestingTWAP.sol"; +import {VestingContextEncoder} from "../src/types/twap/libraries/VestingContextEncoder.sol"; + +contract ComposableCowVestingTWAPTest is Test { + using SafeCast for uint256; + + ComposableCoW composableCow; + VestingTWAP vestingTWAP; + IVestingEscrow vestingEscrow; + address owner; + IERC20 sellToken; + IERC20 buyToken; + bytes32 appDataFirstOrder; + bytes32 appDataTwap; + bytes32 ctx; + + function setUp() public { + vestingTWAP = new VestingTWAP(composableCow); + sellToken = IERC20(address(0x01)); + buyToken = IERC20(address(0x02)); + owner = address(0x03); + appDataFirstOrder = bytes32(abi.encodePacked(address(0x4))); + appDataTwap = bytes32(abi.encodePacked(address(0x5))); + ctx = keccak256("twapvesting"); + } + + function mockTwapVestingData( + uint256 claimAmount, + uint256 minPartLimit, + uint256 minFirstPartLimit, + uint256 span + ) public returns (VestingTWAP.Data memory) { + return + VestingTWAP.Data({ + buyToken: buyToken, + receiver: owner, + vesting: vestingEscrow, + claimAmount: claimAmount, + appDataTwap: appDataTwap, + appDataFirstOrder: appDataFirstOrder, + minPartLimit: minPartLimit, + span: span, + minFirstPartLimit: minFirstPartLimit + }); + } + + function mockContext( + uint256 orderCreationTime, + uint256 totalVested, + uint256 startTime, + uint256 endTime + ) public { + uint256 initialLockedAt = VestingMathLib.lockedAt( + block.timestamp, + startTime, + endTime, + totalVested, + 0 + ); + vm.mockCall( + address(vestingEscrow), + abi.encodeWithSelector(IVestingEscrow.totalLocked.selector), + abi.encode(totalVested) + ); + vm.mockCall( + address(composableCow), + abi.encodeWithSignature("cabinet(address,bytes32)", owner, ctx), + abi.encode( + VestingContextEncoder.encode( + orderCreationTime, + totalVested - initialLockedAt // assuming that the vesting contract was never claimed to simplify the tests + ) + ) + ); + vm.mockCall( + address(vestingEscrow), + abi.encodeWithSelector(IVestingEscrow.startTime.selector), + abi.encode(startTime) + ); + vm.mockCall( + address(vestingEscrow), + abi.encodeWithSelector(IVestingEscrow.endTime.selector), + abi.encode(endTime) + ); + vm.mockCall( + address(vestingEscrow), + abi.encodeWithSelector(IVestingEscrow.cliffLength.selector), + abi.encode(0) + ); + vm.mockCall( + address(vestingEscrow), + abi.encodeWithSelector(IVestingEscrow.token.selector), + abi.encode(sellToken) + ); + } + + function test_VestingMathLib_lockedAt() public { + uint256 startTime = block.timestamp; + uint256 endTime = startTime + 100 days; + uint256 totalLocked = 1e18; + uint256 cliffLength = 10 days; + + assertEq( + VestingMathLib.lockedAt( + startTime, + startTime, + endTime, + totalLocked, + cliffLength + ), + totalLocked + ); + + assertEq( + VestingMathLib.lockedAt( + startTime + cliffLength, + startTime, + endTime, + totalLocked, + cliffLength + ), + totalLocked + ); + + assertEq( + VestingMathLib.lockedAt( + startTime + cliffLength + 1 days, + startTime, + endTime, + totalLocked, + cliffLength + ), + 8.9e17 + ); + + assertEq( + VestingMathLib.lockedAt( + endTime, + startTime, + endTime, + totalLocked, + cliffLength + ), + 0 + ); + + assertEq( + VestingMathLib.lockedAt( + endTime + 1, + startTime, + endTime, + totalLocked, + cliffLength + ), + 0 + ); + } + + function test_VestingMathLib_calculatePeriod() public { + uint256 initialVestingLocked = 1e18; + uint256 endTime = block.timestamp + 100 days; + uint256 orderCreationTime = block.timestamp; + + assertEq( + VestingMathLib.calculatePeriod( + 1e17, + endTime, + orderCreationTime, + initialVestingLocked + ), + 10 days + ); + assertEq( + VestingMathLib.calculatePeriod( + 1e19, + endTime, + orderCreationTime, + initialVestingLocked + ), + 0 + ); + } + + function test_VestingMathLib_calculateBatchLength() public { + uint256 initialVestingLocked = 1e18; + + assertEq( + VestingMathLib.calculateBatchLenght(1e17, initialVestingLocked), + 10 + ); + assertEq( + VestingMathLib.calculateBatchLenght(1e17 + 1, initialVestingLocked), + 9 + ); + assertEq( + VestingMathLib.calculateBatchLenght(1e19, initialVestingLocked), + 0 + ); + } + + function test_getTradeableOrder_concrete() public { + vm.warp(1722878593); + uint256 totalVested = 2e20; + uint256 minFirstPartLimit = 1e7; + uint256 span = 0; + uint256 claimAmount = 1e19; + uint256 minPartLimit = 1e6; + VestingTWAP.Data memory data = mockTwapVestingData( + claimAmount, + minPartLimit, + minFirstPartLimit, + span + ); + bytes memory staticInput = abi.encode(data); + + mockContext( + block.timestamp, + totalVested, + block.timestamp - 10 days, + block.timestamp + 10 days + ); + + // check first order + GPv2Order.Data memory order = vestingTWAP.getTradeableOrder( + owner, + address(0), + ctx, + staticInput, + "" + ); + + assertEq(address(order.buyToken), address(buyToken)); + assertEq(address(order.sellToken), address(sellToken)); + assertEq(order.receiver, data.receiver); + assertEq(order.sellAmount, 1e20); + assertEq(order.buyAmount, data.minFirstPartLimit); + assertGt(order.validTo, block.timestamp); + assertEq(order.appData, data.appDataFirstOrder); + assertEq(uint256(order.feeAmount), 0); + assertEq(uint256(order.kind), uint256(GPv2Order.KIND_SELL)); + assertFalse(order.partiallyFillable); + assertEq( + uint256(order.sellTokenBalance), + uint256(GPv2Order.BALANCE_ERC20) + ); + assertEq( + uint256(order.buyTokenBalance), + uint256(GPv2Order.BALANCE_ERC20) + ); + + // test first twap order + vm.warp(block.timestamp + 2 days); + + // check first order + order = vestingTWAP.getTradeableOrder( + owner, + address(0), + ctx, + staticInput, + "" + ); + + assertEq(address(order.buyToken), address(buyToken)); + assertEq(address(order.sellToken), address(sellToken)); + assertEq(order.receiver, data.receiver); + assertEq(order.sellAmount, claimAmount); + assertEq(order.buyAmount, data.minPartLimit); + assertGt(order.validTo, block.timestamp); + assertEq(order.appData, data.appDataTwap); + assertEq(uint256(order.feeAmount), 0); + assertEq(uint256(order.kind), uint256(GPv2Order.KIND_SELL)); + assertFalse(order.partiallyFillable); + assertEq( + uint256(order.sellTokenBalance), + uint256(GPv2Order.BALANCE_ERC20) + ); + assertEq( + uint256(order.buyTokenBalance), + uint256(GPv2Order.BALANCE_ERC20) + ); + } + + function test_RevertOnVestingEnd_fuzz( + uint256 orderCreationTime, + uint256 vestingStartTime, + uint256 span, + uint256 vestingEndTime + ) public { + vm.assume(orderCreationTime > vestingEndTime); + + // guard against overflow + vm.assume(orderCreationTime < type(uint32).max); + vm.assume(vestingStartTime < type(uint32).max); + vm.assume(vestingEndTime < type(uint32).max); + vm.assume(span < type(uint32).max); + + VestingTWAP.Data memory data = mockTwapVestingData( + 1e16, + 1e16, + 1e16, + span + ); + bytes memory staticInput = abi.encode(data); + + mockContext(orderCreationTime, 1e20, vestingStartTime, vestingEndTime); + + vm.warp(vestingEndTime + span + 1); + + vm.expectRevert( + abi.encodeWithSelector( + IConditionalOrder.OrderNotValid.selector, + VESTING_END + ) + ); + + vestingTWAP.getTradeableOrder(owner, address(0), ctx, staticInput, ""); + } + + function test_RevertLowFirstBatch_concrete() public { + vm.warp(1722878593); + + uint256 totalVested = 1e18; + uint256 claimAmount = 1e17; + uint256 vestingPeriod = 10 days; + VestingTWAP.Data memory data = mockTwapVestingData( + claimAmount, + 0, + 0, + 0 + ); + + bytes memory staticInput = abi.encode(data); + + uint256 twapPeriod = VestingMathLib.calculatePeriod( + claimAmount, + block.timestamp + vestingPeriod, + block.timestamp, + totalVested + ); + + mockContext( + block.timestamp, + totalVested, + block.timestamp, + block.timestamp + vestingPeriod + ); + + vm.expectRevert( + abi.encodeWithSelector( + IConditionalOrder.PollTryAtEpoch.selector, + twapPeriod + block.timestamp, + LOW_FIRST_BATCH + ) + ); + vestingTWAP.getTradeableOrder(owner, address(0), ctx, staticInput, ""); + } + + function test_RevertInvalidClaimAmount_fuzz( + uint256 claimAmount, + uint256 totalVested + ) public { + vm.warp(1722878593); + + vm.assume(claimAmount < 1e40); + vm.assume(totalVested < 1e40); + + vm.assume(claimAmount * 2 > totalVested || claimAmount == 0); + + uint256 startTime = block.timestamp - 10 days; + VestingTWAP.Data memory data = mockTwapVestingData( + claimAmount, + 0, + 0, + 0 + ); + + bytes memory staticInput = abi.encode(data); + + mockContext(block.timestamp, totalVested, startTime, block.timestamp); + + vm.expectRevert( + abi.encodeWithSelector( + IConditionalOrder.OrderNotValid.selector, + INVALID_CLAIM_AMOUNT + ) + ); + vestingTWAP.getTradeableOrder(owner, address(0), ctx, staticInput, ""); + } +} diff --git a/test/libraries/VestingContextEncoder.t.sol b/test/libraries/VestingContextEncoder.t.sol new file mode 100644 index 0000000..fe4b5a4 --- /dev/null +++ b/test/libraries/VestingContextEncoder.t.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "../../src/types/twap/libraries/VestingContextEncoder.sol"; +import "../../src/interfaces/IVestingEscrow.sol"; + +contract VestingContextEncoderTest is Test { + function test_Encode_concrete() public { + uint256 timestamp = 1234567890; // Example timestamp + uint256 value = 1 ether; // Example value + bytes32 encoded = VestingContextEncoder.encode(timestamp, value); + + assertEq( + uint256(encoded) & ((1 << 40) - 1), + timestamp, + "Timestamp encoding failed" + ); + assertEq(uint256(encoded) >> 40, value, "Value encoding failed"); + } + + function test_Decode_concrete() public { + uint256 timestamp = 1234567890; + uint256 value = 1 ether; + bytes32 encoded = VestingContextEncoder.encode(timestamp, value); + + (uint256 decodedTimestamp, uint256 decodedValue) = VestingContextEncoder + .decode(encoded); + + assertEq(decodedTimestamp, timestamp, "Timestamp decoding failed"); + assertEq(decodedValue, value, "Value decoding failed"); + } + + function test_EncodeMaxValue_concrete() public { + uint256 maxTimestamp = (1 << 40) - 1; + uint256 maxValue = (1 << 216) - 1; + bytes32 encoded = VestingContextEncoder.encode(maxTimestamp, maxValue); + + (uint256 decodedTimestamp, uint256 decodedValue) = VestingContextEncoder + .decode(encoded); + + assertEq( + decodedTimestamp, + maxTimestamp, + "Max timestamp encoding/decoding failed" + ); + assertEq(decodedValue, maxValue, "Max value encoding/decoding failed"); + } + + function test_FailOnEncodeTimestampTooLarge_concrete() public { + uint256 tooLargeTimestamp = 1 << 40; + uint256 value = 1 ether; + + vm.expectRevert("Timestamp too large"); + VestingContextEncoder.encode(tooLargeTimestamp, value); + } + + function test_FailOnEncodeValueTooLarge_concrete() public { + uint256 timestamp = 1234567890; + uint256 tooLargeValue = 1 << 216; + + vm.expectRevert("Value too large"); + VestingContextEncoder.encode(timestamp, tooLargeValue); + } + + function test_EncodeDecode_fuzz(uint40 timestamp, uint216 value) public { + bytes32 encoded = VestingContextEncoder.encode(timestamp, value); + (uint256 decodedTimestamp, uint256 decodedValue) = VestingContextEncoder + .decode(encoded); + assertEq( + decodedTimestamp, + timestamp, + "Fuzz: Timestamp encoding/decoding failed" + ); + assertEq(decodedValue, value, "Fuzz: Value encoding/decoding failed"); + } + + function test_EncodeTimestampTooLarge_fuzz(uint256 timestamp) public { + vm.assume(timestamp > ((1 << 40) - 1)); + uint256 value = 1 ether; + + vm.expectRevert("Timestamp too large"); + VestingContextEncoder.encode(timestamp, value); + } + + function test_EncodeValueTooLarge_fuzz(uint256 value) public { + vm.assume(value > ((1 << 216) - 1)); + uint256 timestamp = 1234567890; + + vm.expectRevert("Value too large"); + VestingContextEncoder.encode(timestamp, value); + } +} diff --git a/test/values_factories/VestingContextFactory.t.sol b/test/values_factories/VestingContextFactory.t.sol new file mode 100644 index 0000000..96ce87a --- /dev/null +++ b/test/values_factories/VestingContextFactory.t.sol @@ -0,0 +1,96 @@ +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "../../src/value_factories/VestingContextFactory.sol"; +import "../../src/interfaces/IVestingEscrow.sol"; + +contract VestingContexFactoryTest is Test { + VestingContextFactory public vestingContextFactory; + IVestingEscrow public vestingEscrow; + + function setUp() public { + vestingContextFactory = new VestingContextFactory(); + } + + function test_BytesToAddress_concrete() public { + address testAddress = address( + 0x1234567890123456789012345678901234567890 + ); + bytes memory addressBytes = abi.encodePacked(testAddress); + address result = vestingContextFactory.bytesToAddress(addressBytes); + assertEq(result, testAddress, "bytesToAddress conversion failed"); + } + + function test_GetValue_concrete() public { + uint256 unclaimedValue = 100 ether; + vm.mockCall( + address(vestingEscrow), + abi.encodeWithSelector(IVestingEscrow.unclaimed.selector), + abi.encode(unclaimedValue) + ); + + bytes memory addressBytes = abi.encodePacked(address(vestingEscrow)); + bytes32 result = vestingContextFactory.getValue(addressBytes); + + ( + uint256 timestampResult, + uint256 unclaimedResult + ) = VestingContextEncoder.decode(result); + + assertEq(timestampResult, block.timestamp, "Timestamp mismatch"); + assertEq(unclaimedValue, unclaimedResult, "Unclaimed value mismatch"); + } + + function test_RevertOnValueOverflow_concrete() public { + uint256 overflowValue = uint256(type(uint216).max) + 1; + vm.mockCall( + address(vestingEscrow), + abi.encodeWithSelector(IVestingEscrow.unclaimed.selector), + abi.encode(overflowValue + 1) + ); + + bytes memory addressBytes = abi.encodePacked(address(vestingEscrow)); + vm.expectRevert("Value too large"); + vestingContextFactory.getValue(addressBytes); + } + + function test_FuzzGetValue_fuzz( + uint40 timestampValue, + uint216 unclaimedValue + ) public { + vm.mockCall( + address(vestingEscrow), + abi.encodeWithSelector(IVestingEscrow.unclaimed.selector), + abi.encode(unclaimedValue) + ); + vm.warp(timestampValue); + + bytes memory addressBytes = abi.encodePacked(address(vestingEscrow)); + bytes32 result = vestingContextFactory.getValue(addressBytes); + + ( + uint256 timestampResult, + uint256 unclaimedResult + ) = VestingContextEncoder.decode(result); + + assertEq(timestampResult, timestampValue, "Fuxx: Timestamp mismatch"); + assertEq( + unclaimedResult, + unclaimedValue, + "Fuzz: Unclaimed value mismatch" + ); + } + + function test_RevertOnValueOverflow_fuzz(uint256 overflowValue) public { + vm.assume(overflowValue > type(uint216).max); + vm.mockCall( + address(vestingEscrow), + abi.encodeWithSelector(IVestingEscrow.unclaimed.selector), + abi.encode(overflowValue) + ); + + bytes memory addressBytes = abi.encodePacked(address(vestingEscrow)); + vm.expectRevert("Value too large"); + vestingContextFactory.getValue(addressBytes); + } +}