GroupLP
The GroupLP protocol was introduced to create some simple gamified incentive structures around providing liquidity.
An interesting pattern here is that with the split between GroupLP and OptiVault contracts, we actually have a clone creating a different clone. This takes some careful thought but helps to modularize trusted reusable components.
pragma solidity ^0.8.4;
import '@uniswap/v2-periphery/contracts/interfaces/IERC20.sol';
import '@uniswap/v2-periphery/contracts/interfaces/IWETH.sol';
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';
import '@uniswap/lib/contracts/libraries/TransferHelper.sol';
import "@openzeppelin/contracts/proxy/Clones.sol";
interface IOptiVault {
function initialize(address _token, uint256 _lockupDate, uint256 _minimumTokenCommitment, uint256 _withdrawalsLockedUntilTimestamp, address _balanceLookup) external;
}
contract GroupLP {
receive() external payable {}
address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address private constant operations = 0x133A5437951EE1D312fD36a74481987Ec4Bf8A96;
address private constant optiVaultMaster = 0xC5A00A96E6a7039daD5af5c41584469048B26038; // Address to clone OptiVault from
IERC20 public token; // Token to add LP for
address public tokenSupplier; // Supplier of token
uint256 public committedTokenAmount; // Amount to match
uint256 public goalDate; // Date by which to complete the campaign
uint256 public withdrawalsLockedDuration; // Proposed duration of OptiVault lock
uint256 public withdrawalsLockedUntilTimestamp; // End time plus Duration
IUniswapV2Pair private pair; // Pair to add to
bool private initialized; // Campaign pamaters hav been set
bool public campaignFailed; // Allows recovery of ETH under failure conditions
mapping (address => uint256) public ethContributionOf; // Used as numerator for calculating users shares
uint256 public totalFundingRaised; // Used as denominator for calculating users shares
uint256 public mintedLP; // Amount of LP that has been minted
address public optiVault; // cloned OptiVault contract that holds the minted LP
function initialize(address _pair, address _tokenSupplier, uint256 _goalDate, uint256 _withdrawalsLockedDuration, uint256 _commitment) external payable {
require(!initialized, "GroupLP: Already initialized");
committedTokenAmount = _commitment;
pair = IUniswapV2Pair(_pair);
token = (pair.token0() == WETH) ? IERC20(pair.token1()) : IERC20(pair.token0());
goalDate = _goalDate;
withdrawalsLockedDuration = _withdrawalsLockedDuration;
tokenSupplier = _tokenSupplier;
ethContributionOf[tokenSupplier] = msg.value;
totalFundingRaised = msg.value;
initialized = true;
}
function supplierHasCommitedBalance() public view returns (bool valid) {
// Campaign is valid IFF: (Commited tokens <= Supplier approval <= Supplier balance)
uint256 approval = token.allowance(tokenSupplier, address(this));
uint256 balance = token.balanceOf(tokenSupplier);
valid = committedTokenAmount <= approval && approval <= balance;
}
function getReserves() internal view returns (uint256 ethReserves, uint256 tokenReserves) {
(uint reserveA, uint reserveB, ) = pair.getReserves();
(ethReserves, tokenReserves) = WETH < address(token) ? (reserveA, reserveB) : (reserveB, reserveA);
}
function uniswapQuote(uint amountToken) internal view returns (uint256 amountEth) {
(uint256 ethReserves, uint256 tokenReserves) = getReserves();
amountEth = amountToken * ethReserves / tokenReserves;
}
function ethMatchEstimate() public view returns (uint256 ethGoal) {
ethGoal = uniswapQuote(committedTokenAmount) * 1005 / 1000;
}
function fund() public payable {
require(supplierHasCommitedBalance(), "GroupLP: Supplier is missing tokens");
require(!campaignFailed, "GroupLP: Campaign failed, use recoverETH");
require((ethMatchEstimate() * 110) / 100 >= address(this).balance, "GroupLP: Over funded!");
ethContributionOf[msg.sender] += msg.value;
totalFundingRaised += msg.value;
}
function endFailedCampaign() public {
// Either the campaign is invalid (token supplier's balance or approval has fallen beneath the committed tokens)
// Or the campaign goalDate is past with no LP created
require(!campaignFailed, "GroupLP: Campaign already failed.");
require(mintedLP == 0, "GroupLP: LP already added!");
bool campaignHasExpired = (block.timestamp > (goalDate + 1 hours));
if (!supplierHasCommitedBalance() || campaignHasExpired) {
campaignFailed = true;
}
}
function readyToMint() public view returns (bool ready) {
require(address(this).balance >= ethMatchEstimate(), "GroupLP: Campaign needs more ETH to match supplier's committed tokens");
require(supplierHasCommitedBalance(), "GroupLP: Supplier's token balance or approval has fallen below the required amount.");
return true;
}
function supplyLP(uint minimumEth) public returns (uint256 amountToken, uint256 amountETH) {
require(msg.sender == operations);
require(readyToMint());
address pairAddress = address(pair);
uint256 tokenBalanceOfPairPreSupply = token.balanceOf(pairAddress);
TransferHelper.safeTransferFrom(address(token), tokenSupplier, pairAddress, committedTokenAmount);
uint256 tokenBalanceOfPairPostSupply = token.balanceOf(pairAddress);
amountToken = tokenBalanceOfPairPostSupply - tokenBalanceOfPairPreSupply;
amountETH = uniswapQuote(amountToken);
require(amountETH >= minimumEth, "GroupLP: Tokens must be valued at least the minimum Eth");
IWETH(WETH).deposit{value: amountETH}();
assert(IWETH(WETH).transfer(pairAddress, amountETH));
withdrawalsLockedUntilTimestamp = block.timestamp + withdrawalsLockedDuration;
optiVault = Clones.clone(optiVaultMaster);
mintedLP = IUniswapV2Pair(pairAddress).mint(optiVault);
payable(operations).transfer(address(this).balance); //Excess ETH to operations
IOptiVault(optiVault).initialize(pairAddress, 0, 0, withdrawalsLockedUntilTimestamp, address(this));
}
function recoverETH() public {
require(campaignFailed, "GroupLP: Campaign is active. Use withdrawLP.");
require(ethContributionOf[msg.sender] > 0, "GroupLP: You have recovered all your ETH");
payable(msg.sender).transfer(ethContributionOf[msg.sender]);
ethContributionOf[msg.sender] == 0;
}
function contributionOf(address user) external view returns (uint256 _ethBalance) {
_ethBalance = ethContributionOf[user];
}
function sharesOf(address user) external view returns (uint256 _lpTokenShare) {
_lpTokenShare = (mintedLP / 2) * ethContributionOf[user] / totalFundingRaised;
if (user == tokenSupplier) {
_lpTokenShare += mintedLP / 2;
}
}
}
Last updated