lock()
function and passing in ETH as the token, it is possible to deceive the contract into crediting an account without paying.lock()
and lockFor()
functions are intended for locking ERC20 tokens, not directly for ETH.
If _token
is set to ETH (0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
), the contract will erroneously assume that the user has deposited the specified amount of ETH, and it will be added to balances[_receiver][ETH]
.
@@ 134,139 @@
/**
* @notice Locks a valid token
* @param _token address of token to lock
* @param _amount amount of token to lock
* @param _referral info of the referral. This value will be processed in the backend.
*/
function lock(address _token, uint256 _amount, bytes32 _referral) external {
_processLock(_token, _amount, msg.sender, _referral);
}
@@ 144,150 @@
/**
* @notice Locks a valid token for a given address
* @param _token address of token to lock
* @param _amount amount of token to lock
* @param _for address for which ETH is locked
* @param _referral info of the referral. This value will be processed in the backend.
*/
function lockFor(address _token, uint256 _amount, address _for, bytes32 _referral) external {
_processLock(_token, _amount, _for, _referral);
}
@@ 155,162 @@
/**
* @dev Generic internal locking function that updates rewards based on
* previous balances, then update balances.
* @param _token Address of the token to lock
* @param _amount Units of ETH or token to add to the users balance
* @param _receiver Address of user who will receive the stake
* @param _referral Address of the referral user
*/
function _processLock(address _token, uint256 _amount, address _receiver, bytes32 _referral)
internal
onlyBeforeDate(loopActivation)
{
if (_amount == 0) {
revert CannotLockZero();
}
if (_token == ETH) {
totalSupply = totalSupply + _amount;
balances[_receiver][ETH] = balances[_receiver][_token] + _amount;
} else {
@@ 174,185 @@
if (!isTokenAllowed[_token]) {
revert TokenNotAllowed();
}
IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
if (_token == address(WETH)) {
WETH.withdraw(_amount);
totalSupply = totalSupply + _amount;
balances[_receiver][ETH] = balances[_receiver][_token] + _amount;
} else {
balances[_receiver][_token] = balances[_receiver][_token] + _amount;
}
}
emit Locked(_receiver, _amount, _token, _referral);
}
/**
* @dev Called by a owner to convert all the locked ETH to get lpETH
*/
function convertAllETH() external onlyAuthorized {
if (block.timestamp - loopActivation <= TIMELOCK) {
revert LoopNotActivated();
}
// deposits all the ETH to lpETH contract. Receives lpETH back
uint256 totalBalance = address(this).balance;
lpETH.deposit{value: totalBalance}(address(this));
totalLpETH = lpETH.balanceOf(address(this));
// Claims of lpETH can start immediately after conversion.
startClaimDate = uint32(block.timestamp);
emit Converted(totalBalance, totalLpETH);
}
Consider adding validation of require _token != ETH
in lock()
and lockFor()
.
When locking eth
again, due to misuse of the balances[_receiver][ETH]
storage, all the user's previously locked eth
balance will be erased and frozen in the contract.
This is because L182 uses the wrong storage variable.
function _processLock(address _token, uint256 _amount, address _receiver, bytes32 _referral)
internal
onlyBeforeDate(loopActivation)
{
if (_amount == 0) {
revert CannotLockZero();
}
if (_token == ETH) {
totalSupply = totalSupply + _amount;
balances[_receiver][ETH] = balances[_receiver][_token] + _amount;
} else {
if (!isTokenAllowed[_token]) {
revert TokenNotAllowed();
}
IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
if (_token == address(WETH)) {
WETH.withdraw(_amount);
totalSupply = totalSupply + _amount;
balances[_receiver][ETH] = balances[_receiver][_token] + _amount;
} else {
balances[_receiver][_token] = balances[_receiver][_token] + _amount;
}
}
emit Locked(_receiver, _amount, _token, _referral);
}
Consider changing _processLock()
to:
function _processLock(address _token, uint256 _amount, address _receiver, bytes32 _referral)
internal
onlyBeforeDate(loopActivation)
{
if (_amount == 0) {
revert CannotLockZero();
}
if (_token == ETH) {
totalSupply += _amount;
balances[_receiver][ETH] += _amount;
} else {
if (!isTokenAllowed[_token]) {
revert TokenNotAllowed();
}
IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
if (_token == address(WETH)) {
WETH.withdraw(_amount);
totalSupply += _amount;
balances[_receiver][ETH] += _amount;
} else {
balances[_receiver][_token] += _amount;
}
}
emit Locked(_receiver, _amount, _token, _referral);
}
_fillQuote()
allowing outputToken == address(WETH)
without converting to NATIVE token through WETH.withdraw()
According to the implementation of _validateData()
from L414 to L461, it supports _swapCallData
with outputToken == address(WETH)
. However, _fillQuote()
does not convert WETH to NATIVE token in this scenario. This leads to _claim()
at L251 and L254 unexpectedly executing lpETH.deposit{value: 0}(_receiver);
, resulting in the loss of WETH obtained from the swap. This portion of WETH will be locked in the PrelaunchPoints contract, inaccessible to anyone.
/**
* @dev Called by a user to get their vested lpETH
* @param _token Address of the token to convert to lpETH
* @param _percentage Proportion in % of tokens to withdraw. NOT useful for ETH
* @param _exchange Exchange identifier where the swap takes place
* @param _data Swap data obtained from 0x API
*/
function claim(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)
external
onlyAfterDate(startClaimDate)
{
_claim(_token, msg.sender, _percentage, _exchange, _data);
}
/**
* @dev Called by a user to get their vested lpETH and stake them in a
* Loop vault for extra rewards
* @param _token Address of the token to convert to lpETH
* @param _percentage Proportion in % of tokens to withdraw. NOT useful for ETH
* @param _exchange Exchange identifier where the swap takes place
* @param _data Swap data obtained from 0x API
*/
function claimAndStake(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)
external
onlyAfterDate(startClaimDate)
{
uint256 claimedAmount = _claim(_token, address(this), _percentage, _exchange, _data);
lpETH.approve(address(lpETHVault), claimedAmount);
lpETHVault.stake(claimedAmount, msg.sender);
emit StakedVault(msg.sender, claimedAmount);
}
/**
* @dev Claim logic. If necessary converts token to ETH before depositing into lpETH contract.
*/
function _claim(address _token, address _receiver, uint8 _percentage, Exchange _exchange, bytes calldata _data)
internal
returns (uint256 claimedAmount)
{
uint256 userStake = balances[msg.sender][_token];
if (userStake == 0) {
revert NothingToClaim();
}
if (_token == ETH) {
claimedAmount = userStake.mulDiv(totalLpETH, totalSupply);
balances[msg.sender][_token] = 0;
lpETH.safeTransfer(_receiver, claimedAmount);
} else {
uint256 userClaim = userStake * _percentage / 100;
_validateData(_token, userClaim, _exchange, _data);
balances[msg.sender][_token] = userStake - userClaim;
// Swap token to ETH
uint256 totalETH = address(this).balance;
_fillQuote(IERC20(_token), userClaim, _data);
claimedAmount = address(this).balance - totalETH;
// Convert swapped ETH to lpETH (1 to 1 conversion)
lpETH.deposit{value: claimedAmount}(_receiver);
}
emit Claimed(msg.sender, _token, claimedAmount);
}
/**
* @notice Validates the data sent from 0x API to match desired behaviour
* @param _token address of the token to sell
* @param _amount amount of token to sell
* @param _exchange exchange identifier where the swap takes place
* @param _data swap data from 0x API
*/
function _validateData(address _token, uint256 _amount, Exchange _exchange, bytes calldata _data) internal view {
address inputToken;
address outputToken;
uint256 inputTokenAmount;
address recipient;
bytes4 selector;
if (_exchange == Exchange.UniswapV3) {
(inputToken, outputToken, inputTokenAmount, recipient, selector) = _decodeUniswapV3Data(_data);
if (selector != UNI_SELECTOR) {
revert WrongSelector(selector);
}
} else if (_exchange == Exchange.TransformERC20) {
(inputToken, outputToken, inputTokenAmount, selector) = _decodeTransformERC20Data(_data);
if (selector != TRANSFORM_SELECTOR) {
revert WrongSelector(selector);
}
} else {
revert WrongExchange();
}
if (inputToken != _token || (outputToken != ETH && outputToken != address(WETH))) {
revert WrongDataTokens(inputToken, outputToken);
}
if (inputTokenAmount != _amount) {
revert WrongDataAmount(inputTokenAmount);
}
if (recipient != address(this) && recipient != address(0)) {
revert WrongRecipient(recipient);
}
}
/**
*
* @param _sellToken The `sellTokenAddress` field from the API response.
* @param _amount The `sellAmount` field from the API response.
* @param _swapCallData The `data` field from the API response.
*/
function _fillQuote(IERC20 _sellToken, uint256 _amount, bytes calldata _swapCallData) internal {
// Track our balance of the buyToken to determine how much we've bought.
uint256 boughtETHAmount = address(this).balance;
require(_sellToken.approve(exchangeProxy, _amount));
(bool success,) = payable(exchangeProxy).call{value: 0}(_swapCallData);
if (!success) {
revert SwapCallFailed();
}
// Use our current buyToken balance to determine how much we've bought.
boughtETHAmount = address(this).balance - boughtETHAmount;
emit SwappedTokens(address(_sellToken), boughtETHAmount);
}
_claim()
function calculates the received lpETH
differently for cases when _token == ETH
and _token != ETH
.At line 254, there's an implicit assumption that depositing 1 wei of NATIVE token into lpETH.deposit()
yields 1 wei of lpETH
, in contrast to line 240, which does not make this assumption (otherwise, line 240 could simply return userStake
as the amount of lpETH
received).
If, at the time of executing line 254, the exchange rate between lpETH and NATIVE token is not 1:1, the amount of lpETH
returned will be incorrect. This leads to an incorrect claimedAmount
being used in line 223's lpETHVault.stake(claimedAmount, msg.sender);
, potentially resulting in lost lpETH
or an unexpected revert.
/**
* @dev Called by a user to get their vested lpETH and stake them in a
* Loop vault for extra rewards
* @param _token Address of the token to convert to lpETH
* @param _percentage Proportion in % of tokens to withdraw. NOT useful for ETH
* @param _exchange Exchange identifier where the swap takes place
* @param _data Swap data obtained from 0x API
*/
function claimAndStake(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)
external
onlyAfterDate(startClaimDate)
{
uint256 claimedAmount = _claim(_token, address(this), _percentage, _exchange, _data);
lpETH.approve(address(lpETHVault), claimedAmount);
lpETHVault.stake(claimedAmount, msg.sender);
emit StakedVault(msg.sender, claimedAmount);
}
/**
* @dev Claim logic. If necessary converts token to ETH before depositing into lpETH contract.
*/
function _claim(address _token, address _receiver, uint8 _percentage, Exchange _exchange, bytes calldata _data)
internal
returns (uint256 claimedAmount)
{
uint256 userStake = balances[msg.sender][_token];
if (userStake == 0) {
revert NothingToClaim();
}
if (_token == ETH) {
claimedAmount = userStake.mulDiv(totalLpETH, totalSupply);
balances[msg.sender][_token] = 0;
lpETH.safeTransfer(_receiver, claimedAmount);
} else {
uint256 userClaim = userStake * _percentage / 100;
_validateData(_token, userClaim, _exchange, _data);
balances[msg.sender][_token] = userStake - userClaim;
// Swap token to ETH
uint256 totalETH = address(this).balance;
_fillQuote(IERC20(_token), userClaim, _data);
claimedAmount = address(this).balance - totalETH;
// Convert swapped ETH to lpETH (1 to 1 conversion)
lpETH.deposit{value: claimedAmount}(_receiver);
}
emit Claimed(msg.sender, _token, claimedAmount);
}
/**
* @dev Called by a owner to convert all the locked ETH to get lpETH
*/
function convertAllETH() external onlyAuthorized {
if (block.timestamp - loopActivation <= TIMELOCK) {
revert LoopNotActivated();
}
// deposits all the ETH to lpETH contract. Receives lpETH back
uint256 totalBalance = address(this).balance;
lpETH.deposit{value: totalBalance}(address(this));
totalLpETH = lpETH.balanceOf(address(this));
// Claims of lpETH can start immediately after conversion.
startClaimDate = uint32(block.timestamp);
emit Converted(totalBalance, totalLpETH);
}
Consider revising the _claim()
function around line 254 to also use the method of calculating received lpETH
amounts through lpETH.balanceOf()
as done in convertAllETH()
at line 312.
Alternatively, if lpETH is a rebase token (like stETH), it's necessary to improve the maintenance of , not by using the total number of lpETH, totalLpETH, but rather the internal share number of lpETH, totalLpETHShare.
emergencyMode
may necessitate blocking claims.If the scenario involves convertAllETH
having already occurred, and the project team has transferred ETH back to the current contract, then activates emergencyMode
, withdraw()
ETH in this situation would cause a decrease in totalSupply, thereby affecting claims (by miscalculating the exchange rate).
function withdraw(address _token) external {
if (!emergencyMode) {
if (block.timestamp <= loopActivation) {
revert CurrentlyNotPossible();
}
if (block.timestamp >= startClaimDate) {
revert NoLongerPossible();
}
}
uint256 lockedAmount = balances[msg.sender][_token];
balances[msg.sender][_token] = 0;
if (lockedAmount == 0) {
revert CannotWithdrawZero();
}
if (_token == ETH) {
totalSupply = totalSupply - lockedAmount;
(bool sent,) = msg.sender.call{value: lockedAmount}("");
if (!sent) {
revert FailedToSendEther();
}
} else {
IERC20(_token).safeTransfer(msg.sender, lockedAmount);
}
emit Withdrawn(msg.sender, _token, lockedAmount);
}
convertAllETH()
should not be allowed to be called more than once.Otherwise, it may inadvertently affect .
That's because once convertAllETH()
is called, claim()
will be made available, and claiming can change the lpETH balance. If convertAllETH()
gets called again, the ratio of will be changed unexpectedly.
For example, here is a case where accidentally decreases:
Given:
balances[Alice][ETH]
: 100e18
balances[Bob][ETH]
: 100e18
totalSupply
: 200e18
address(PrelaunchPoints).balance
: 200e18
When:
convertAllETH()
call
lpETH.deposit{value: 200e18}(address(this));
200e18
lpETHtotalLpETH
to 200e18
claim(ETH, ...)
claimedAmount = userStake.mulDiv(totalLpETH, totalSupply) = 100e18 * 200e18 / 200e18 = 100e18
100e18
lpETH to Alice
200e18
to 100e18
10e18
of NATIVE tokenconvertAllETH()
call
lpETH.deposit{value: 10e18}(address(this));
10e18
lpETH, lpETH.balanceOf(PrelaunchPoints) changes from 100e18
to 110e18
totalLpETH
to 110e18
claim(ETH, ...)
claimedAmount = userStake.mulDiv(totalLpETH, totalSupply) = 100e18 * 110e18 / 200e18 = 55e18
55e18
lpETH to Bob
110e18
to 55e18
Summary:
balances[msg.sender][_token]
but receive different amounts of lpETH55e18
lpETH /**
* @dev Called by a owner to convert all the locked ETH to get lpETH
*/
function convertAllETH() external onlyAuthorized {
if (block.timestamp - loopActivation <= TIMELOCK) {
revert LoopNotActivated();
}
// deposits all the ETH to lpETH contract. Receives lpETH back
uint256 totalBalance = address(this).balance;
lpETH.deposit{value: totalBalance}(address(this));
totalLpETH = lpETH.balanceOf(address(this));
// Claims of lpETH can start immediately after conversion.
startClaimDate = uint32(block.timestamp);
emit Converted(totalBalance, totalLpETH);
}
@@ 195,201 @@
/**
* @dev Called by a user to get their vested lpETH
* @param _token Address of the token to convert to lpETH
* @param _percentage Proportion in % of tokens to withdraw. NOT useful for ETH
* @param _exchange Exchange identifier where the swap takes place
* @param _data Swap data obtained from 0x API
*/
function claim(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)
external
onlyAfterDate(startClaimDate)
{
_claim(_token, msg.sender, _percentage, _exchange, _data);
}
@@ 209,230 @@
/**
* @dev Called by a user to get their vested lpETH and stake them in a
* Loop vault for extra rewards
* @param _token Address of the token to convert to lpETH
* @param _percentage Proportion in % of tokens to withdraw. NOT useful for ETH
* @param _exchange Exchange identifier where the swap takes place
* @param _data Swap data obtained from 0x API
*/
function claimAndStake(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)
external
onlyAfterDate(startClaimDate)
{
uint256 claimedAmount = _claim(_token, address(this), _percentage, _exchange, _data);
lpETH.approve(address(lpETHVault), claimedAmount);
lpETHVault.stake(claimedAmount, msg.sender);
emit StakedVault(msg.sender, claimedAmount);
}
/**
* @dev Claim logic. If necessary converts token to ETH before depositing into lpETH contract.
*/
function _claim(address _token, address _receiver, uint8 _percentage, Exchange _exchange, bytes calldata _data)
internal
returns (uint256 claimedAmount)
{
uint256 userStake = balances[msg.sender][_token];
if (userStake == 0) {
revert NothingToClaim();
}
if (_token == ETH) {
claimedAmount = userStake.mulDiv(totalLpETH, totalSupply);
balances[msg.sender][_token] = 0;
lpETH.safeTransfer(_receiver, claimedAmount);
} else {
@@ 244,254 @@
uint256 userClaim = userStake * _percentage / 100;
_validateData(_token, userClaim, _exchange, _data);
balances[msg.sender][_token] = userStake - userClaim;
// Swap token to ETH
uint256 totalETH = address(this).balance;
_fillQuote(IERC20(_token), userClaim, _data);
claimedAmount = address(this).balance - totalETH;
// Convert swapped ETH to lpETH (1 to 1 conversion)
lpETH.deposit{value: claimedAmount}(_receiver);
}
emit Claimed(msg.sender, _token, claimedAmount);
}
address(this).balance
) as the asset of msg.sender
when _claim()
is invoked.We assume that there are no NATIVE tokens in the PrelaunchPoints contract when _claim()
is reached at L249. All the address(this).balance
at that time, must belong to msg.sender
.
Due to the restrictions at L204 and L219, claiming is only possible when block.timestamp > startClaimDate
, and startClaimDate
is set in convertAllETH()
at L315. This means that claims can only be made (ignoring the special case where the initial value of startClaimDate at L100 is 4294967295 (2106-02-07T06:28:15.000Z) without convertAllETH()
) after the execution of convertAllETH()
. The convertAllETH()
at L310 will deplete all NATIVE tokens in the PrelaunchPoints contract.
Furthermore, the condition block.timestamp - loopActivation > TIMELOCK
at L304 must be met to execute convertAllETH()
, making it impossible to lockETH()
/ lockETHFor()
which require block.timestamp < loopActivation
at L165 (in other words, at this point, the PrelaunchPoints contract can only receive NATIVE tokens through receive() external payable {}
).
@@ 195,201 @@
/**
* @dev Called by a user to get their vested lpETH
* @param _token Address of the token to convert to lpETH
* @param _percentage Proportion in % of tokens to withdraw. NOT useful for ETH
* @param _exchange Exchange identifier where the swap takes place
* @param _data Swap data obtained from 0x API
*/
function claim(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)
external
onlyAfterDate(startClaimDate)
{
_claim(_token, msg.sender, _percentage, _exchange, _data);
}
@@ 209,216 @@
/**
* @dev Called by a user to get their vested lpETH and stake them in a
* Loop vault for extra rewards
* @param _token Address of the token to convert to lpETH
* @param _percentage Proportion in % of tokens to withdraw. NOT useful for ETH
* @param _exchange Exchange identifier where the swap takes place
* @param _data Swap data obtained from 0x API
*/
function claimAndStake(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)
external
onlyAfterDate(startClaimDate)
{
uint256 claimedAmount = _claim(_token, address(this), _percentage, _exchange, _data);
lpETH.approve(address(lpETHVault), claimedAmount);
lpETHVault.stake(claimedAmount, msg.sender);
emit StakedVault(msg.sender, claimedAmount);
}
@@ 228,230 @@
/**
* @dev Claim logic. If necessary converts token to ETH before depositing into lpETH contract.
*/
function _claim(address _token, address _receiver, uint8 _percentage, Exchange _exchange, bytes calldata _data)
internal
returns (uint256 claimedAmount)
{
uint256 userStake = balances[msg.sender][_token];
if (userStake == 0) {
revert NothingToClaim();
}
if (_token == ETH) {
claimedAmount = userStake.mulDiv(totalLpETH, totalSupply);
balances[msg.sender][_token] = 0;
lpETH.safeTransfer(_receiver, claimedAmount);
} else {
uint256 userClaim = userStake * _percentage / 100;
_validateData(_token, userClaim, _exchange, _data);
balances[msg.sender][_token] = userStake - userClaim;
// Swap token to ETH
uint256 totalETH = address(this).balance;
_fillQuote(IERC20(_token), userClaim, _data);
claimedAmount = address(this).balance - totalETH;
// Convert swapped ETH to lpETH (1 to 1 conversion)
lpETH.deposit{value: claimedAmount}(_receiver);
}
emit Claimed(msg.sender, _token, claimedAmount);
}
/**
* @dev Called by a owner to convert all the locked ETH to get lpETH
*/
function convertAllETH() external onlyAuthorized {
if (block.timestamp - loopActivation <= TIMELOCK) {
revert LoopNotActivated();
}
// deposits all the ETH to lpETH contract. Receives lpETH back
uint256 totalBalance = address(this).balance;
lpETH.deposit{value: totalBalance}(address(this));
totalLpETH = lpETH.balanceOf(address(this));
// Claims of lpETH can start immediately after conversion.
startClaimDate = uint32(block.timestamp);
emit Converted(totalBalance, totalLpETH);
}
/**
* @notice Locks ETH
* @param _referral info of the referral. This value will be processed in the backend.
*/
function lockETH(bytes32 _referral) external payable {
_processLock(ETH, msg.value, msg.sender, _referral);
}
/**
* @notice Locks ETH for a given address
* @param _for address for which ETH is locked
* @param _referral info of the referral. This value will be processed in the backend.
*/
function lockETHFor(address _for, bytes32 _referral) external payable {
_processLock(ETH, msg.value, _for, _referral);
}
@@ 134,162 @@
/**
* @notice Locks a valid token
* @param _token address of token to lock
* @param _amount amount of token to lock
* @param _referral info of the referral. This value will be processed in the backend.
*/
function lock(address _token, uint256 _amount, bytes32 _referral) external {
_processLock(_token, _amount, msg.sender, _referral);
}
/**
* @notice Locks a valid token for a given address
* @param _token address of token to lock
* @param _amount amount of token to lock
* @param _for address for which ETH is locked
* @param _referral info of the referral. This value will be processed in the backend.
*/
function lockFor(address _token, uint256 _amount, address _for, bytes32 _referral) external {
_processLock(_token, _amount, _for, _referral);
}
/**
* @dev Generic internal locking function that updates rewards based on
* previous balances, then update balances.
* @param _token Address of the token to lock
* @param _amount Units of ETH or token to add to the users balance
* @param _receiver Address of user who will receive the stake
* @param _referral Address of the referral user
*/
function _processLock(address _token, uint256 _amount, address _receiver, bytes32 _referral)
internal
onlyBeforeDate(loopActivation)
{
@@ 167,188 @@
if (_amount == 0) {
revert CannotLockZero();
}
if (_token == ETH) {
totalSupply = totalSupply + _amount;
balances[_receiver][ETH] = balances[_receiver][_token] + _amount;
} else {
if (!isTokenAllowed[_token]) {
revert TokenNotAllowed();
}
IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
if (_token == address(WETH)) {
WETH.withdraw(_amount);
totalSupply = totalSupply + _amount;
balances[_receiver][ETH] = balances[_receiver][_token] + _amount;
} else {
balances[_receiver][_token] = balances[_receiver][_token] + _amount;
}
}
emit Locked(_receiver, _amount, _token, _referral);
}
Consider changing _claim()
to:
@@ 195,201 @@
/**
* @dev Called by a user to get their vested lpETH
* @param _token Address of the token to convert to lpETH
* @param _percentage Proportion in % of tokens to withdraw. NOT useful for ETH
* @param _exchange Exchange identifier where the swap takes place
* @param _data Swap data obtained from 0x API
*/
function claim(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)
external
onlyAfterDate(startClaimDate)
{
_claim(_token, msg.sender, _percentage, _exchange, _data);
}
@@ 209,216 @@
/**
* @dev Called by a user to get their vested lpETH and stake them in a
* Loop vault for extra rewards
* @param _token Address of the token to convert to lpETH
* @param _percentage Proportion in % of tokens to withdraw. NOT useful for ETH
* @param _exchange Exchange identifier where the swap takes place
* @param _data Swap data obtained from 0x API
*/
function claimAndStake(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)
external
onlyAfterDate(startClaimDate)
{
uint256 claimedAmount = _claim(_token, address(this), _percentage, _exchange, _data);
lpETH.approve(address(lpETHVault), claimedAmount);
lpETHVault.stake(claimedAmount, msg.sender);
emit StakedVault(msg.sender, claimedAmount);
}
@@ 228,230 @@
/**
* @dev Claim logic. If necessary converts token to ETH before depositing into lpETH contract.
*/
function _claim(address _token, address _receiver, uint8 _percentage, Exchange _exchange, bytes calldata _data)
internal
returns (uint256 claimedAmount)
{
uint256 userStake = balances[msg.sender][_token];
if (userStake == 0) {
revert NothingToClaim();
}
if (_token == ETH) {
claimedAmount = userStake.mulDiv(totalLpETH, totalSupply);
balances[msg.sender][_token] = 0;
lpETH.safeTransfer(_receiver, claimedAmount);
} else {
uint256 userClaim = userStake * _percentage / 100;
_validateData(_token, userClaim, _exchange, _data);
balances[msg.sender][_token] = userStake - userClaim;
// Swap token to ETH
_fillQuote(IERC20(_token), userClaim, _data);
// Convert swapped ETH to lpETH (1 to 1 conversion)
// assume all ETH balance is from the swap above
lpETH.deposit{value: address(this).balance}(_receiver);
}
emit Claimed(msg.sender, _token, claimedAmount);
}
inputTokenAmount
to the SwappedTokens
event.outputTokenAmount
is only meaningful when there is a corresponding inputTokenAmount
context.inputTokenAmount
is not in the calldata, making it inconvenient to obtain through other means. event SwappedTokens(address sellToken, uint256 buyETHAmount);
/**
*
* @param _sellToken The `sellTokenAddress` field from the API response.
* @param _amount The `sellAmount` field from the API response.
* @param _swapCallData The `data` field from the API response.
*/
function _fillQuote(IERC20 _sellToken, uint256 _amount, bytes calldata _swapCallData) internal {
// Track our balance of the buyToken to determine how much we've bought.
uint256 boughtETHAmount = address(this).balance;
require(_sellToken.approve(exchangeProxy, _amount));
(bool success,) = payable(exchangeProxy).call{value: 0}(_swapCallData);
if (!success) {
revert SwapCallFailed();
}
// Use our current buyToken balance to determine how much we've bought.
boughtETHAmount = address(this).balance - boughtETHAmount;
emit SwappedTokens(address(_sellToken), boughtETHAmount);
}
@@ 195,230 @@
/**
* @dev Called by a user to get their vested lpETH
* @param _token Address of the token to convert to lpETH
* @param _percentage Proportion in % of tokens to withdraw. NOT useful for ETH
* @param _exchange Exchange identifier where the swap takes place
* @param _data Swap data obtained from 0x API
*/
function claim(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)
external
onlyAfterDate(startClaimDate)
{
_claim(_token, msg.sender, _percentage, _exchange, _data);
}
/**
* @dev Called by a user to get their vested lpETH and stake them in a
* Loop vault for extra rewards
* @param _token Address of the token to convert to lpETH
* @param _percentage Proportion in % of tokens to withdraw. NOT useful for ETH
* @param _exchange Exchange identifier where the swap takes place
* @param _data Swap data obtained from 0x API
*/
function claimAndStake(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)
external
onlyAfterDate(startClaimDate)
{
uint256 claimedAmount = _claim(_token, address(this), _percentage, _exchange, _data);
lpETH.approve(address(lpETHVault), claimedAmount);
lpETHVault.stake(claimedAmount, msg.sender);
emit StakedVault(msg.sender, claimedAmount);
}
/**
* @dev Claim logic. If necessary converts token to ETH before depositing into lpETH contract.
*/
function _claim(address _token, address _receiver, uint8 _percentage, Exchange _exchange, bytes calldata _data)
internal
returns (uint256 claimedAmount)
{
uint256 userStake = balances[msg.sender][_token];
if (userStake == 0) {
revert NothingToClaim();
}
if (_token == ETH) {
@@ 240,242 @@
claimedAmount = userStake.mulDiv(totalLpETH, totalSupply);
balances[msg.sender][_token] = 0;
lpETH.safeTransfer(_receiver, claimedAmount);
} else {
uint256 userClaim = userStake * _percentage / 100;
_validateData(_token, userClaim, _exchange, _data);
balances[msg.sender][_token] = userStake - userClaim;
// Swap token to ETH
uint256 totalETH = address(this).balance;
_fillQuote(IERC20(_token), userClaim, _data);
claimedAmount = address(this).balance - totalETH;
// Convert swapped ETH to lpETH (1 to 1 conversion)
lpETH.deposit{value: claimedAmount}(_receiver);
}
emit Claimed(msg.sender, _token, claimedAmount);
}