Skip to content
Open
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
14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,26 @@
"postinstall": "husky install",
"precommit": "lint-staged",
"vercel": "yarn workspace @se-2/nextjs vercel",
"vercel:yolo": "yarn workspace @se-2/nextjs vercel:yolo"
"vercel:yolo": "yarn workspace @se-2/nextjs vercel:yolo",
"produce-signature": "node --loader ts-node/esm scripts/produceSignature.ts"
},
"packageManager": "[email protected]",
"devDependencies": {
"@types/node": "^22.7.4",
"axios": "^1.4.0",
"form-data": "^4.0.0",
"husky": "^8.0.1",
"lint-staged": "^13.0.3"
"lint-staged": "^13.0.3",
"ts-node": "latest",
"typescript": "^5.6.2"
},
"resolutions": {
"usehooks-ts@^2.7.2": "patch:usehooks-ts@npm:^2.7.2#./.yarn/patches/usehooks-ts-npm-2.7.2-fceffe0e43.patch"
},
"type": "module",
"dependencies": {
"@wagmi/core": "^2.13.8",
"dotenv": "^16.4.5",
"viem": "2.x"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;

import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import { SafeERC20 } from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import "@openzeppelin/contracts/utils/Strings.sol";

import {IFeeLevelJudge} from "../IFeeLevelJudge.sol";
import "../factory/ContinuousVestingInitializable.sol";
import "../../utilities/AccessVerifier.sol";
import "../../interfaces/IOracleOrL2OracleWithSequencerCheck.sol";
import "../../config/INetworkConfig.sol";

contract ContinuousVestingMerkleDistributor_v_5_0 is Initializable, ContinuousVestingInitializable, AccessVerifier {
using Address for address payable;
using SafeERC20 for IERC20;

uint256 internal constant NATIVE_TOKEN_DECIMALS = 18;
INetworkConfig networkConfig;
// denominator used to determine size of fee bips
uint256 constant feeFractionDenominator = 10000;

constructor() {
_disableInitializers();
}

function initialize(
IERC20 _token, // the token being claimed
uint256 _total, // the total claimable by all users
string memory _uri, // information on the sale (e.g. merkle proofs)
uint256 _start, // vesting clock starts at this time
uint256 _cliff, // claims open at this time
uint256 _end, // vesting clock ends and this time
bytes32 _merkleRoot, // (deprecated) the merkle root for claim membership (also used as salt for the fair queue delay time),
uint160 _maxDelayTime, // the maximum delay time for the fair queue
address _owner,
address _feeOrSupplyHolder,
bool _autoPull,
INetworkConfig _networkConfig
) public initializer {
__ContinuousVesting_init(
_token, _total, _uri, _start, _cliff, _end, _maxDelayTime, uint160(uint256(_merkleRoot)), _owner
);

_transferOwnership(_owner);

networkConfig = _networkConfig;

IFeeLevelJudge feeLevelJudge = IFeeLevelJudge(networkConfig.getStakingAddress());
uint256 feeLevel = feeLevelJudge.getFeeLevel(_msgSender());
if (feeLevel == 0) {
feeLevel = 100;
}

// TODO: reduce duplication with other contracts
uint256 feeAmount = (_total * feeLevel) / feeFractionDenominator;
if (_autoPull) {
_token.safeTransferFrom(_feeOrSupplyHolder, address(this), _total + feeAmount);

_token.approve(address(this), 0);
_token.approve(address(this), feeAmount);
_token.safeTransferFrom(address(this), networkConfig.getFeeRecipient(), feeAmount);
} else {
_token.safeTransferFrom(_feeOrSupplyHolder, address(this), feeAmount);
}
}

function NAME() external pure override returns (string memory) {
return "ContinuousVestingMerkleInitializable";
}

function VERSION() external pure override returns (uint256) {
return 5;
}

modifier validSignature(uint256 totalAmount, uint64 expiresAt, bytes memory signature) {
verifyAccessSignature(networkConfig.getAccessAuthorityAddress(), _msgSender(), totalAmount, expiresAt, signature);

_;
}

function claim(
address beneficiary, // the address that will receive tokens
uint256 totalAmount, // the total claimable by this beneficiary
uint64 expiresAt,
bytes memory signature,
address payable platformFlatRateFeeRecipient,
uint256 platformFlatRateFeeAmount
)
external
payable
validSignature(totalAmount, expiresAt, signature)
nonReentrant
{
IOracleOrL2OracleWithSequencerCheck nativeTokenPriceOracle = IOracleOrL2OracleWithSequencerCheck(networkConfig.getNativeTokenPriceOracleAddress());
uint256 nativeTokenPriceOracleHeartbeat = networkConfig.getNativeTokenPriceOracleHeartbeat();

uint256 baseCurrencyValue = tokensToBaseCurrency(
msg.value,
NATIVE_TOKEN_DECIMALS,
nativeTokenPriceOracle,
nativeTokenPriceOracleHeartbeat
);

require(baseCurrencyValue >= platformFlatRateFeeAmount, "fee payment below minimum");

uint256 feeAmountInWei = ((platformFlatRateFeeAmount * (10 ** NATIVE_TOKEN_DECIMALS)) / getOraclePrice(nativeTokenPriceOracle, nativeTokenPriceOracleHeartbeat));

platformFlatRateFeeRecipient.sendValue(feeAmountInWei);
payable(_msgSender()).sendValue(msg.value - feeAmountInWei);

// effects
uint256 claimedAmount = super._executeClaim(beneficiary, totalAmount);
// interactions
_settleClaim(beneficiary, claimedAmount);
}

// TODO: reduce duplication between other contracts
function tokensToBaseCurrency(
uint256 tokenQuantity,
uint256 tokenDecimals,
IOracleOrL2OracleWithSequencerCheck oracle,
uint256 heartbeat
) public view returns (uint256 value) {
return (tokenQuantity * getOraclePrice(oracle, heartbeat)) / (10**tokenDecimals);
}

// TODO: reduce duplication between other contracts
// Get a positive token price from a chainlink oracle
function getOraclePrice(IOracleOrL2OracleWithSequencerCheck oracle, uint256 heartbeat) public view returns (uint256) {
(
uint80 roundID,
int256 _price,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
) = oracle.latestRoundData();

require(_price > 0, "negative price");
require(answeredInRound > 0, "answer == 0");
require(updatedAt > 0, "round not complete");
require(answeredInRound >= roundID, "stale price");
require(updatedAt < block.timestamp - heartbeat, "stale price");

return uint256(_price);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";

import "../../interfaces/IOracleOrL2OracleWithSequencerCheck.sol";
import {INetworkConfig} from "../../config/INetworkConfig.sol";
import {ContinuousVestingMerkleDistributor_v_5_0} from "./ContinuousVestingMerkleDistributor.sol";

contract ContinuousVestingMerkleDistributorFactory_v_5_0 {
using Address for address payable;

address private immutable i_implementation;
address[] public distributors;

uint256 internal constant NATIVE_TOKEN_DECIMALS = 18;

event DistributorDeployed(address indexed distributor);

constructor(address implementation) {
i_implementation = implementation;
}

function _getSalt(
IERC20 _token, // the token being claimed
uint256 _total, // the total claimable by all users
string memory _uri, // information on the sale (e.g. merkle proofs)
uint256 _start, // vesting clock starts at this time
uint256 _cliff, // claims open at this time
uint256 _end, // vesting clock ends and this time
bytes32 _merkleRoot, // the merkle root for claim membership (also used as salt for the fair queue delay time),
uint160 _maxDelayTime, // the maximum delay time for the fair queue
address _owner,
address _feeOrSupplyHolder,
bool _autoPull,
INetworkConfig _networkConfig,
address payable _platformFlatRateFeeRecipient,
uint256 _platformFlatRateFeeAmount,
uint256 _nonce
) private pure returns (bytes32) {
return keccak256(abi.encode(
_token,
_total,
_uri,
_start,
_cliff,
_end,
_merkleRoot,
_maxDelayTime,
_owner,
_feeOrSupplyHolder,
_autoPull,
_networkConfig,
_platformFlatRateFeeRecipient,
_platformFlatRateFeeAmount,
_nonce
));
}

function deployDistributor(
IERC20 _token, // the token being claimed
uint256 _total, // the total claimable by all users
string memory _uri, // information on the sale (e.g. merkle proofs)
uint256 _start, // vesting clock starts at this time
uint256 _cliff, // claims open at this time
uint256 _end, // vesting clock ends and this time
bytes32 _merkleRoot, // the merkle root for claim membership (also used as salt for the fair queue delay time),
uint160 _maxDelayTime, // the maximum delay time for the fair queue
address _owner,
address _feeOrSupplyHolder,
bool _autoPull,
INetworkConfig _networkConfig,
address payable _platformFlatRateFeeRecipient,
uint256 _platformFlatRateFeeAmount,
uint256 _nonce
) public payable returns (ContinuousVestingMerkleDistributor_v_5_0 distributor) {
IOracleOrL2OracleWithSequencerCheck nativeTokenPriceOracle = IOracleOrL2OracleWithSequencerCheck(_networkConfig.getNativeTokenPriceOracleAddress());
uint256 nativeTokenPriceOracleHeartbeat = _networkConfig.getNativeTokenPriceOracleHeartbeat();

uint256 nativeBaseCurrencyValue = tokensToBaseCurrency(
msg.value,
NATIVE_TOKEN_DECIMALS,
nativeTokenPriceOracle,
nativeTokenPriceOracleHeartbeat
);

require(nativeBaseCurrencyValue >= _platformFlatRateFeeAmount, "fee payment below minimum");

uint256 feeAmountInWei = ((_platformFlatRateFeeAmount * (10 ** NATIVE_TOKEN_DECIMALS)) / getOraclePrice(nativeTokenPriceOracle, nativeTokenPriceOracleHeartbeat));

_platformFlatRateFeeRecipient.sendValue(feeAmountInWei);
payable(msg.sender).sendValue(msg.value - feeAmountInWei);

bytes32 salt = _getSalt(
_token,
_total,
_uri,
_start,
_cliff,
_end,
_merkleRoot,
_maxDelayTime,
_owner,
_feeOrSupplyHolder,
_autoPull,
_networkConfig,
_platformFlatRateFeeRecipient,
_platformFlatRateFeeAmount,
_nonce
);

distributor =
ContinuousVestingMerkleDistributor_v_5_0(Clones.cloneDeterministic(i_implementation, salt));
distributors.push(address(distributor));

emit DistributorDeployed(address(distributor));

distributor.initialize(_token, _total, _uri, _start, _cliff, _end, _merkleRoot, _maxDelayTime, _owner, _feeOrSupplyHolder, _autoPull, _networkConfig);

return distributor;
}

function getImplementation() public view returns (address) {
return i_implementation;
}

function predictDistributorAddress(
IERC20 _token, // the token being claimed
uint256 _total, // the total claimable by all users
string memory _uri, // information on the sale (e.g. merkle proofs)
uint256 _start, // vesting clock starts at this time
uint256 _cliff, // claims open at this time
uint256 _end, // vesting clock ends and this time
bytes32 _merkleRoot, // the merkle root for claim membership (also used as salt for the fair queue delay time),
uint160 _maxDelayTime, // the maximum delay time for the fair queue
address _owner,
address _feeOrSupplyHolder,
bool _autoPull,
INetworkConfig _networkConfig,
address payable _platformFlatRateFeeRecipient,
uint256 _platformFlatRateFeeAmount,
uint256 _nonce
) public view returns (address) {
bytes32 salt = _getSalt(
_token,
_total,
_uri,
_start,
_cliff,
_end,
_merkleRoot,
_maxDelayTime,
_owner,
_feeOrSupplyHolder,
_autoPull,
_networkConfig,
_platformFlatRateFeeRecipient,
_platformFlatRateFeeAmount,
_nonce
);

return Clones.predictDeterministicAddress(i_implementation, salt, address(this));
}

function tokensToBaseCurrency(
uint256 tokenQuantity,
uint256 tokenDecimals,
IOracleOrL2OracleWithSequencerCheck oracle,
uint256 heartbeat
) public view returns (uint256 value) {
return (tokenQuantity * getOraclePrice(oracle, heartbeat)) / (10**tokenDecimals);
}

function getOraclePrice(IOracleOrL2OracleWithSequencerCheck oracle, uint256 heartbeat) public view returns (uint256) {
(
uint80 roundID,
int256 _price,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
) = oracle.latestRoundData();

require(_price > 0, "negative price");
require(answeredInRound > 0, "answer == 0");
require(updatedAt > 0, "round not complete");
require(answeredInRound >= roundID, "stale price");
require(updatedAt < block.timestamp - heartbeat, "stale price");

return uint256(_price);
}
}
Loading
Loading