Skip to content
Closed
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
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@ FORK_RPC_ENDPOINT=
MONGO_URI=mongodb://3ac:3ac@db:27017
SERVER_HOST=0.0.0.0
SERVER_PORT=4000
GNOSIS_RPC_ENDPOINT=https://rpc.gnosischain.com

ETHEREUM_RPC_URL=https://ethereum.publicnode.com
GNOSIS_RPC_URL=https://rpc.gnosischain.com
ARBITRUM_RPC_URL=https://arb1.arbitrum.io/rpc
BASE_RPC_URL=https://mainnet.base.org
10 changes: 3 additions & 7 deletions packages/contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,15 @@ test = 'tests'
libs = ['lib']
out = 'out'
optimizer = true
solc_version = '0.8.20'
evm_version = 'london'
solc_version = '0.8.28'
evm_version = 'cancun'
eth_rpc_url = 'https://rpc.gnosischain.com'

[rpc_endpoints]
ethereum = "${ETHEREUM_RPC_URL}"
gnosis = "${GNOSIS_RPC_URL}"
arbitrum = "${ARBITRUM_RPC_URL}"

[etherscan]
ethereum = { key = "${ETHERSCAN_API_KEY}" }
gnosis = { key = "${GNOSISSCAN_API_KEY}" }
arbitrum = { key = "${ARBITRUM_API_KEY}" }
base = "${BASE_RPC_URL}"

[fmt]
tab_width = 2
121 changes: 75 additions & 46 deletions packages/contracts/src/DCAOrder.sol
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

import {IERC20} from "oz/token/ERC20/IERC20.sol";
import {SafeERC20} from "oz/token/ERC20/utils/SafeERC20.sol";
import {IGPv2Settlement} from "./interfaces/IGPv2Settlement.sol";
import {IConditionalOrder} from "./interfaces/IConditionalOrder.sol";
import {IDCAOrder} from "./interfaces/IDCAOrder.sol";
import {GPv2Order} from "./libraries/GPv2Order.sol";
import {GPv2EIP1271, EIP1271Verifier} from "./interfaces/EIP1271Verifier.sol";
import {BokkyPooBahsDateTimeLibrary} from "date/BokkyPooBahsDateTimeLibrary.sol";
import {SafeMath} from "oz/utils/math/SafeMath.sol";
import {Math} from "oz/utils/math/Math.sol";
pragma solidity 0.8.28;

import {IERC20} from 'oz/token/ERC20/IERC20.sol';
import {SafeERC20} from 'oz/token/ERC20/utils/SafeERC20.sol';
import {IGPv2Settlement} from './interfaces/IGPv2Settlement.sol';
import {IConditionalOrder} from './interfaces/IConditionalOrder.sol';
import {IDCAOrder} from './interfaces/IDCAOrder.sol';
import {GPv2Order} from './libraries/GPv2Order.sol';
import {GPv2EIP1271, EIP1271Verifier} from './interfaces/EIP1271Verifier.sol';
import {BokkyPooBahsDateTimeLibrary} from 'date/BokkyPooBahsDateTimeLibrary.sol';
import {SafeMath} from 'oz/utils/math/SafeMath.sol';
import {Math} from 'oz/utils/math/Math.sol';

error OrderCancelled();
error NotOwner();
Expand Down Expand Up @@ -49,7 +49,17 @@ contract DCAOrder is IConditionalOrder, EIP1271Verifier, IDCAOrder {
/// @dev The initial amount of the DCA order.
uint256 public amount;

event Initialized(address indexed order);
event Initialized(
address indexed order,
address owner,
address receiver,
address sellToken,
address buyToken,
uint256 amount,
uint256 startTime,
uint256 endTime,
uint256 interval
);
event Cancelled(address indexed order);

/// @dev Initializes the DCAOrder with the specified parameters.
Expand Down Expand Up @@ -112,7 +122,17 @@ contract DCAOrder is IConditionalOrder, EIP1271Verifier, IDCAOrder {
IERC20(_sellToken).safeApprove(address(IGPv2Settlement(_settlementContract).vaultRelayer()), type(uint256).max);
emit ConditionalOrderCreated(address(this)); // Required by COW to watch this contract
// Emit Initialized event for indexing
emit Initialized(address(this));
emit Initialized(
address(this), // order
_owner, // owner
_receiver, // receiver
_sellToken, // sellToken
_buyToken, // buyToken
_amount, // amount
_startTime, // startTime
_endTime, // endTime
_interval // interval
);
return true;
}

Expand Down Expand Up @@ -149,46 +169,52 @@ contract DCAOrder is IConditionalOrder, EIP1271Verifier, IDCAOrder {
// ensures that orders queried shortly after one another result in the same hash (to avoid spamming the orderbook)
// solhint-disable-next-line not-rely-on-time
uint32 currentTimeBucket = ((uint32(orderExecutionTime) / 900) + 1) * 900;
return GPv2Order.Data(
sellToken,
buyToken,
receiver, // The receiver
orderSellAmount,
1, // 0 buy amount is not allowed
currentTimeBucket + 900, // between 15 and 30 miunte validity
keccak256("DollarCostAveraging"),
0,
GPv2Order.KIND_SELL,
false,
GPv2Order.BALANCE_ERC20,
GPv2Order.BALANCE_ERC20
);
return
GPv2Order.Data(
sellToken,
buyToken,
receiver, // The receiver
orderSellAmount,
1, // 0 buy amount is not allowed
currentTimeBucket + 900, // between 15 and 30 miunte validity
keccak256('DollarCostAveraging'),
0,
GPv2Order.KIND_SELL,
false,
GPv2Order.BALANCE_ERC20,
GPv2Order.BALANCE_ERC20
);
// uint32 currentTimeBucket = ((uint32(block.timestamp) / 900) + 1) * 900;
}

/// @param orderDigest The EIP-712 signing digest derived from the order
/// @param encodedOrder Bytes-encoded order information, originally created by an off-chain bot. Created by concatening the order data (in the form of GPv2Order.Data), the price checker address, and price checker data.
function isValidSignature(bytes32 orderDigest, bytes calldata encodedOrder) external view override returns (bytes4) {
GPv2Order.Data memory order = abi.decode(encodedOrder, (GPv2Order.Data));
require(order.hash(domainSeparator) == orderDigest, "encoded order digest mismatch");
require(order.hash(domainSeparator) == orderDigest, 'encoded order digest mismatch');

// If getTradeableOrder() may change between blocks (e.g. because of a variable exchange rate or exprity date, perform a proper attribute comparison with `order` here instead of matching full hashes)
require(
IConditionalOrder(this).getTradeableOrder().hash(domainSeparator) == orderDigest,
"encoded order != tradable order"
'encoded order != tradable order'
);

return GPv2EIP1271.MAGICVALUE;
}

/// @dev get the total number of orders that will be executed between the start and end time
function orderSlots() public view returns (uint256[] memory slots) {
uint256 total = Math.ceilDiv(BokkyPooBahsDateTimeLibrary.diffHours(startTime, endTime), interval);
uint256 _startTime = startTime;
uint256 _interval = interval;
uint256 intervalHours = _interval * 1 hours;
uint256 total = Math.ceilDiv(BokkyPooBahsDateTimeLibrary.diffHours(_startTime, endTime), _interval);

slots = new uint256[](total);
// Create execution orders

uint256 nextSlot = _startTime;
for (uint256 i = 0; i < total; i++) {
uint256 orderExecutionTime = startTime + (i * interval * 1 hours);
slots[i] = orderExecutionTime;
slots[i] = nextSlot;
nextSlot += intervalHours;
}
return slots;
}
Expand All @@ -197,6 +223,7 @@ contract DCAOrder is IConditionalOrder, EIP1271Verifier, IDCAOrder {
/// @dev a slot is consider current if the current time is greater than the slot time and less than the next slot time (if it exists)
function currentSlot() public view returns (uint256 slot) {
uint256 _startTime = startTime;
uint256 _endTime = endTime;
uint256 currentTime = block.timestamp;

// Calculate the next time slot based on the current time
Expand All @@ -205,10 +232,10 @@ contract DCAOrder is IConditionalOrder, EIP1271Verifier, IDCAOrder {
}

// If the curernt time is beyond the end time, return 0 indicating no further time slots
if (currentTime > endTime) {
if (currentTime > _endTime) {
return 0;
}

uint256 intervalTimestamp = interval * 1 hours;
return _startTime + (((currentTime - _startTime) / intervalTimestamp) * intervalTimestamp);
}
Expand All @@ -217,25 +244,27 @@ contract DCAOrder is IConditionalOrder, EIP1271Verifier, IDCAOrder {
/// @return bool True if the current timestamp corresponds to the last time slot, otherwise false.
function isLastSlot() public view returns (bool) {
uint256 intervalTimestamp = interval * 1 hours;
return ((startTime + ((((block.timestamp - startTime) / intervalTimestamp) + 1) * intervalTimestamp)) + intervalTimestamp) > endTime;
return
((startTime + ((((block.timestamp - startTime) / intervalTimestamp) + 1) * intervalTimestamp)) +
intervalTimestamp) > endTime;
}

/// @dev returns the sell amount for the each slot
function slotSellAmount() public view returns (uint256 orderSellAmount) {
function slotSellAmount() public view returns (uint256) {
// Execute at the specified frequency
// Each order sellAmount is the balance of the order divided by the frequency
// If the current slot is the last slot, the returned amount is the total sellToken balance
uint256 _endTime = endTime;
// solhint-disable-next-line not-rely-on-time

if (block.timestamp >= endTime) {
return 0;
}

if (isLastSlot()) {
return sellToken.balanceOf(address(this));
}

if (block.timestamp >= _endTime) {
return 0;
}

// amount divided by total amount of orders
(, orderSellAmount) = SafeMath.tryDiv(amount, (Math.ceilDiv(BokkyPooBahsDateTimeLibrary.diffHours(startTime, _endTime), interval)));
// Use storage variables directly since they're only accessed once
uint256 totalSlots = Math.ceilDiv(BokkyPooBahsDateTimeLibrary.diffHours(startTime, endTime), interval);
return amount / totalSlots;
}
}
2 changes: 1 addition & 1 deletion packages/contracts/src/OrderFactory.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.20;
pragma solidity 0.8.28;

import {Clones} from 'oz/proxy/Clones.sol';
import {IERC20} from 'oz/token/ERC20/IERC20.sol';
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/src/interfaces/EIP1271Verifier.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
pragma solidity ^0.8.20;
pragma solidity ^0.8.28;

library GPv2EIP1271 {
/// @dev Value returned by a call to `isValidSignature` if the signature
Expand Down
4 changes: 2 additions & 2 deletions packages/contracts/src/interfaces/IConditionalOrder.sol
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
pragma solidity ^0.8.28;
pragma abicoder v2;

import {GPv2Order} from "../libraries/GPv2Order.sol";
import {GPv2Order} from '../libraries/GPv2Order.sol';

interface IConditionalOrder {
/// Event that should be emitted in constructor so that the service "watching" for conditional orders can start indexing it
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/src/interfaces/IDCAOrder.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
pragma solidity ^0.8.28;
pragma abicoder v2;

interface IDCAOrder {
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/src/interfaces/IGPv2Settlement.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
pragma solidity ^0.8.28;

interface IGPv2Settlement {
/// @dev Sets a presignature for the specified order UID.
Expand Down
30 changes: 14 additions & 16 deletions packages/contracts/src/libraries/GPv2Order.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
pragma solidity 0.8.20;
pragma solidity 0.8.28;

import {IERC20} from "oz/interfaces/IERC20.sol";
import {IERC20} from 'oz/interfaces/IERC20.sol';

/// @title Gnosis Protocol v2 Order Library
/// @author Gnosis Developers
Expand Down Expand Up @@ -44,7 +44,7 @@ library GPv2Order {
/// ")"
/// )
/// ```
bytes32 internal constant TYPE_HASH = hex"d5a25ba2e97094ad7d83dc28a6572da797d6b3e7fc6663bd93efb789fc17e489";
bytes32 internal constant TYPE_HASH = hex'd5a25ba2e97094ad7d83dc28a6572da797d6b3e7fc6663bd93efb789fc17e489';

/// @dev The marker value for a sell order for computing the order struct
/// hash. This allows the EIP-712 compatible wallets to display a
Expand All @@ -54,7 +54,7 @@ library GPv2Order {
/// ```
/// keccak256("sell")
/// ```
bytes32 internal constant KIND_SELL = hex"f3b277728b3fee749481eb3e0b3b48980dbbab78658fc419025cb16eee346775";
bytes32 internal constant KIND_SELL = hex'f3b277728b3fee749481eb3e0b3b48980dbbab78658fc419025cb16eee346775';

/// @dev The OrderKind marker value for a buy order for computing the order
/// struct hash.
Expand All @@ -63,7 +63,7 @@ library GPv2Order {
/// ```
/// keccak256("buy")
/// ```
bytes32 internal constant KIND_BUY = hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc";
bytes32 internal constant KIND_BUY = hex'6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc';

/// @dev The TokenBalance marker value for using direct ERC20 balances for
/// computing the order struct hash.
Expand All @@ -72,7 +72,7 @@ library GPv2Order {
/// ```
/// keccak256("erc20")
/// ```
bytes32 internal constant BALANCE_ERC20 = hex"5a28e9363bb942b639270062aa6bb295f434bcdfc42c97267bf003f272060dc9";
bytes32 internal constant BALANCE_ERC20 = hex'5a28e9363bb942b639270062aa6bb295f434bcdfc42c97267bf003f272060dc9';

/// @dev The TokenBalance marker value for using Balancer Vault external
/// balances (in order to re-use Vault ERC20 approvals) for computing the
Expand All @@ -82,7 +82,7 @@ library GPv2Order {
/// ```
/// keccak256("external")
/// ```
bytes32 internal constant BALANCE_EXTERNAL = hex"abee3b73373acd583a130924aad6dc38cfdc44ba0555ba94ce2ff63980ea0632";
bytes32 internal constant BALANCE_EXTERNAL = hex'abee3b73373acd583a130924aad6dc38cfdc44ba0555ba94ce2ff63980ea0632';

/// @dev The TokenBalance marker value for using Balancer Vault internal
/// balances for computing the order struct hash.
Expand All @@ -91,7 +91,7 @@ library GPv2Order {
/// ```
/// keccak256("internal")
/// ```
bytes32 internal constant BALANCE_INTERNAL = hex"4ac99ace14ee0a5ef932dc609df0943ab7ac16b7583634612f8dc35a4289a6ce";
bytes32 internal constant BALANCE_INTERNAL = hex'4ac99ace14ee0a5ef932dc609df0943ab7ac16b7583634612f8dc35a4289a6ce';

/// @dev Marker address used to indicate that the receiver of the trade
/// proceeds should the owner of the order.
Expand Down Expand Up @@ -145,7 +145,7 @@ library GPv2Order {
// solhint-disable-next-line no-inline-assembly
assembly {
let freeMemoryPointer := mload(0x40)
mstore(freeMemoryPointer, "\x19\x01")
mstore(freeMemoryPointer, '\x19\x01')
mstore(add(freeMemoryPointer, 2), domainSeparator)
mstore(add(freeMemoryPointer, 34), structHash)
orderDigest := keccak256(freeMemoryPointer, 66)
Expand All @@ -164,7 +164,7 @@ library GPv2Order {
/// @param owner The address of the user who owns this order.
/// @param validTo The epoch time at which the order will stop being valid.
function packOrderUidParams(bytes memory orderUid, bytes32 orderDigest, address owner, uint32 validTo) internal pure {
require(orderUid.length == UID_LENGTH, "GPv2: uid buffer overflow");
require(orderUid.length == UID_LENGTH, 'GPv2: uid buffer overflow');

// NOTE: Write the order UID to the allocated memory buffer. The order
// parameters are written to memory in **reverse order** as memory
Expand Down Expand Up @@ -206,12 +206,10 @@ library GPv2Order {
/// parameters.
/// @return owner The address of the user who owns this order.
/// @return validTo The epoch time at which the order will stop being valid.
function extractOrderUidParams(bytes calldata orderUid)
internal
pure
returns (bytes32 orderDigest, address owner, uint32 validTo)
{
require(orderUid.length == UID_LENGTH, "GPv2: invalid uid");
function extractOrderUidParams(
bytes calldata orderUid
) internal pure returns (bytes32 orderDigest, address owner, uint32 validTo) {
require(orderUid.length == UID_LENGTH, 'GPv2: invalid uid');

// Use assembly to efficiently decode packed calldata.
// solhint-disable-next-line no-inline-assembly
Expand Down
Loading
Loading