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
934 changes: 912 additions & 22 deletions .openzeppelin/base-sepolia.json

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions contracts/token/ICumulativeMerkleDrop.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;
pragma abicoder v1;

// Allows anyone to claim a token if they exist in a merkle root.
interface ICumulativeMerkleDrop {
// This event is triggered whenever a call to #setMerkleRoot succeeds.
event MerkelRootUpdated(bytes32 oldMerkleRoot, bytes32 newMerkleRoot);
// This event is triggered whenever a call to #claim succeeds.
event Claimed(address indexed account, uint256 amount);

error InvalidProof();
error NothingToClaim();
error MerkleRootWasUpdated();

// Returns the address of the token distributed by this contract.
function token() external view returns (address);
// Returns the merkle root of the merkle tree containing cumulative account balances available to claim.
function merkleRoot() external view returns (bytes32);
// Sets the merkle root of the merkle tree containing cumulative account balances available to claim.
function setMerkleRoot(bytes32 merkleRoot_) external;
// Claim the given amount of the token to the given address. Reverts if the inputs are invalid.
function claim(
address account,
uint256 cumulativeAmount,
bytes32 expectedMerkleRoot,
bytes32[] calldata merkleProof
) external;
}
135 changes: 135 additions & 0 deletions contracts/token/cumulativeMerkleDrop.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {
SafeERC20,
IERC20
} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
// import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

import {ICumulativeMerkleDrop} from "./ICumulativeMerkleDrop.sol";
import "./veVirtual.sol";

contract CumulativeMerkleDrop is Ownable, ICumulativeMerkleDrop {
using SafeERC20 for IERC20;
// using MerkleProof for bytes32[];

// solhint-disable-next-line immutable-vars-naming
address public immutable override token;
address public immutable veVirtualContract;

bytes32 public override merkleRoot;
mapping(address => uint256) public cumulativeClaimed;

constructor(
address token_,
address veVirtualContract_
) Ownable(msg.sender) {
token = token_;
veVirtualContract = veVirtualContract_;
}

function setMerkleRoot(bytes32 merkleRoot_) external override onlyOwner {
emit MerkelRootUpdated(merkleRoot, merkleRoot_);
merkleRoot = merkleRoot_;
}

/// @notice Claim tokens and stake them in veVirtual as eco trader lock
/// @dev Anyone can call this for any account if they have valid merkleProof
/// Tokens will be claimed and automatically staked in veVirtual with autoRenew = true
/// The tokens should already be in this contract (injected by backend weekly)
/// @param account The account to claim and stake for
/// @param cumulativeAmount The cumulative amount the account can claim
/// @param expectedMerkleRoot The expected merkle root (must match current merkleRoot)
/// @param merkleProof The merkle proof for the claim
function claimAndMaxStake(
address account,
uint256 cumulativeAmount,
bytes32 expectedMerkleRoot,
bytes32[] calldata merkleProof
) public {
if (merkleRoot != expectedMerkleRoot) revert MerkleRootWasUpdated();

// Verify the merkle proof
bytes32 leaf = keccak256(abi.encodePacked(account, cumulativeAmount));
if (!_verifyAsm(merkleProof, expectedMerkleRoot, leaf))
revert InvalidProof();

// Mark it claimed
uint256 preclaimed = cumulativeClaimed[account];
if (preclaimed >= cumulativeAmount) revert NothingToClaim();
cumulativeClaimed[account] = cumulativeAmount;

// Stake the token
unchecked {
uint256 amount = cumulativeAmount - preclaimed;
// Approve veVirtual contract to spend tokens from this contract
IERC20(token).forceApprove(veVirtualContract, amount);

// Call stakeEcoLockFor on veVirtual contract
// This will transfer tokens from this contract and create an eco lock for the account
veVirtual(veVirtualContract).stakeEcoLockFor(account, amount);

emit Claimed(account, amount);
}
}

function claim(
address account,
uint256 cumulativeAmount,
bytes32 expectedMerkleRoot,
bytes32[] calldata merkleProof
) external override {
claimAndMaxStake(
account,
cumulativeAmount,
expectedMerkleRoot,
merkleProof
);
}

// function verify(bytes32[] calldata merkleProof, bytes32 root, bytes32 leaf) public pure returns (bool) {
// return merkleProof.verify(root, leaf);
// }

function _verifyAsm(
bytes32[] calldata proof,
bytes32 root,
bytes32 leaf
) private pure returns (bool valid) {
/// @solidity memory-safe-assembly
assembly {
// solhint-disable-line no-inline-assembly
let ptr := proof.offset
for {
let end := add(ptr, mul(0x20, proof.length))
} lt(ptr, end) {
ptr := add(ptr, 0x20)
} {
let node := calldataload(ptr)

switch lt(leaf, node)
case 1 {
mstore(0x00, leaf)
mstore(0x20, node)
}
default {
mstore(0x00, node)
mstore(0x20, leaf)
}
leaf := keccak256(0x00, 0x40)
}

valid := eq(root, leaf)
}
}

function adminWithdraw(
address tokenAddress,
uint256 amount
) external onlyOwner {
IERC20(tokenAddress).safeTransfer(msg.sender, amount);
}
}
60 changes: 59 additions & 1 deletion contracts/token/veVirtual.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ contract veVirtual is
uint8 numWeeks; // Active duration in weeks. Reset to maxWeeks if autoRenew is true.
bool autoRenew;
uint256 id;
bool isEco; // If true, this is an eco lock managed by ecoVeVirtualStaker, cannot be withdrawn by user
}

uint16 public constant DENOM = 10000;
Expand All @@ -45,7 +46,14 @@ contract veVirtual is
event AutoRenew(address indexed user, uint256 id, bool autoRenew);

event AdminUnlocked(bool adminUnlocked);
event EcoLockUpdated(
address indexed user,
uint256 id,
uint256 amount,
uint8 numWeeks
);
bool public adminUnlocked;
mapping(address => Lock) public ecoLocks; // Separate mapping for eco locks (one per trader)

function initialize(
address baseToken_,
Expand Down Expand Up @@ -98,6 +106,7 @@ contract veVirtual is
for (uint i = 0; i < locks[account].length; i++) {
balance += _balanceOfLockAt(locks[account][i], timestamp);
}
balance += _balanceOfLockAt(ecoLocks[account], timestamp);
return balance;
}

Expand Down Expand Up @@ -163,13 +172,27 @@ contract veVirtual is
end: end,
numWeeks: numWeeks,
autoRenew: autoRenew,
id: _nextId++
id: _nextId++,
isEco: false
});
locks[_msgSender()].push(lock);
emit Stake(_msgSender(), lock.id, amount, numWeeks);
_transferVotingUnits(address(0), _msgSender(), amount);
}

function stakeEcoLockFor(
address account,
uint256 amount
) external nonReentrant {
require(amount > 0, "Amount must be greater than 0");
require(account != address(0), "Invalid account");

// Transfer tokens from caller
IERC20(baseToken).safeTransferFrom(_msgSender(), address(this), amount);

_increaseEcoLockAmount(account, amount);
}

function _calcValue(
uint256 amount,
uint8 numWeeks
Expand Down Expand Up @@ -199,6 +222,7 @@ contract veVirtual is
address account = _msgSender();
uint256 index = _indexOf(account, id);
Lock memory lock = locks[account][index];
require(!lock.isEco, "Cannot withdraw eco lock"); // redundant check since eco locks are not in locks[] array
require(
block.timestamp >= lock.end || adminUnlocked,
"Lock is not expired"
Expand All @@ -223,6 +247,7 @@ contract veVirtual is
uint256 index = _indexOf(account, id);

Lock storage lock = locks[account][index];
require(!lock.isEco, "Cannot modify eco lock"); // redundant check since eco locks are not in locks[] array
lock.autoRenew = !lock.autoRenew;
lock.numWeeks = maxWeeks;
lock.start = block.timestamp;
Expand All @@ -235,6 +260,7 @@ contract veVirtual is
address account = _msgSender();
uint256 index = _indexOf(account, id);
Lock storage lock = locks[account][index];
require(!lock.isEco, "Cannot modify eco lock"); // redundant check since eco locks are not in locks[] array
require(lock.autoRenew == false, "Lock is auto-renewing");
require(block.timestamp < lock.end, "Lock is expired");
require(
Expand Down Expand Up @@ -296,6 +322,38 @@ contract veVirtual is
for (uint i = 0; i < locks[account].length; i++) {
amount += locks[account][i].amount;
}
amount += ecoLocks[account].amount;
return amount;
}

function _increaseEcoLockAmount(address account, uint256 amount) internal {
Lock storage existingLock = ecoLocks[account];
if (existingLock.id == 0) {
// Create new eco lock
Lock memory newLock = Lock({
amount: amount,
start: block.timestamp,
end: block.timestamp + uint256(maxWeeks) * 1 weeks,
numWeeks: maxWeeks,
autoRenew: true,
id: _nextId++,
isEco: true
});
ecoLocks[account] = newLock;
} else {
// Update existing eco lock
existingLock.amount += amount;
existingLock.start = block.timestamp;
existingLock.end = block.timestamp + uint256(maxWeeks) * 1 weeks;
}

emit EcoLockUpdated(
account,
ecoLocks[account].id,
ecoLocks[account].amount,
maxWeeks
);
// add new voting units
_transferVotingUnits(address(0), account, amount);
}
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
"hardhat-contract-sizer": "^2.10.0",
"hardhat-deploy": "^0.11.45",
"hardhat-gas-reporter": "^1.0.8",
"keccak256": "^1.0.6",
"merkletreejs": "^0.6.0",
"solidity-coverage": "^0.8.1",
"ts-node": "^10.9.2",
"typechain": "^8.2.0",
Expand Down
Loading