Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions script/deploy_VestingTwap.s.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}
19 changes: 19 additions & 0 deletions src/interfaces/IVestingEscrow.sol
Original file line number Diff line number Diff line change
@@ -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);
}
174 changes: 174 additions & 0 deletions src/types/twap/VestingTWAP.sol
Original file line number Diff line number Diff line change
@@ -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
});
}
}
32 changes: 32 additions & 0 deletions src/types/twap/libraries/VestingContextEncoder.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
57 changes: 57 additions & 0 deletions src/types/twap/libraries/VestingMathLib.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
31 changes: 31 additions & 0 deletions src/value_factories/VestingContextFactory.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading