aave-protocol-v2/contracts/protocol/tokenization/StaticATokenLM.sol
2021-06-04 09:20:37 +02:00

613 lines
22 KiB
Solidity

// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.6.12;
pragma experimental ABIEncoderV2;
import {ILendingPool} from '../../interfaces/ILendingPool.sol';
import {IERC20} from '../../dependencies/openzeppelin/contracts/IERC20.sol';
import {IAToken} from '../../interfaces/IAToken.sol';
import {IAaveIncentivesController} from '../../interfaces/IAaveIncentivesController.sol';
import {ERC20} from '../../dependencies/openzeppelin/contracts/ERC20.sol';
import {SafeERC20} from '../../dependencies/openzeppelin/contracts/SafeERC20.sol';
import {WadRayMath} from '../../protocol/libraries/math/WadRayMath.sol';
import {RayMathNoRounding} from '../../protocol/libraries/math/RayMathNoRounding.sol';
import {SafeMath} from '../../dependencies/openzeppelin/contracts/SafeMath.sol';
/**
* @title StaticATokenLM
* @dev Wrapper token that allows to deposit tokens on the Aave protocol and receive
* a token which balance doesn't increase automatically, but uses an ever-increasing exchange rate.
* The token support claiming liquidity mining rewards from the Aave system.
* @author Aave
**/
contract StaticATokenLM is ERC20 {
using SafeERC20 for IERC20;
using SafeMath for uint256;
using WadRayMath for uint256;
using RayMathNoRounding for uint256;
struct SignatureParams {
uint8 v;
bytes32 r;
bytes32 s;
}
bytes public constant EIP712_REVISION = bytes('1');
bytes32 internal constant EIP712_DOMAIN =
keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)');
bytes32 public constant PERMIT_TYPEHASH =
keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)');
bytes32 public constant METADEPOSIT_TYPEHASH =
keccak256(
'Deposit(address depositor,address recipient,uint256 value,uint16 referralCode,bool fromUnderlying,uint256 nonce,uint256 deadline)'
);
bytes32 public constant METAWITHDRAWAL_TYPEHASH =
keccak256(
'Withdraw(address owner,address recipient,uint256 staticAmount,uint256 dynamicAmount,bool toUnderlying,uint256 nonce,uint256 deadline)'
);
ILendingPool public immutable LENDING_POOL;
IERC20 public immutable ATOKEN;
IERC20 public immutable ASSET;
mapping(address => uint256) public _nonces;
uint256 public rewardIndex;
uint256 public lifeTimeRewardsClaimed;
uint256 public lifeTimeRewards;
uint256 public lastRewardBlock;
// user => userIndex (in RAYs)
mapping(address => uint256) private _userIndex;
// user => unclaimedRewards (in RAYs)
mapping(address => uint256) private _unclaimedRewards;
IAaveIncentivesController internal _incentivesController;
address public immutable currentRewardToken;
constructor(
ILendingPool lendingPool,
address aToken,
string memory wrappedTokenName,
string memory wrappedTokenSymbol
) public ERC20(wrappedTokenName, wrappedTokenSymbol) {
LENDING_POOL = lendingPool;
ATOKEN = IERC20(aToken);
IERC20 underlyingAsset = IERC20(IAToken(aToken).UNDERLYING_ASSET_ADDRESS());
ASSET = underlyingAsset;
underlyingAsset.approve(address(lendingPool), type(uint256).max);
_incentivesController = IAToken(aToken).getIncentivesController();
currentRewardToken = _incentivesController.REWARD_TOKEN();
}
/**
* @dev Deposits `ASSET` in the Aave protocol and mints static aTokens to msg.sender
* @param recipient The address that will receive the static aTokens
* @param amount The amount of underlying `ASSET` to deposit (e.g. deposit of 100 USDC)
* @param referralCode Code used to register the integrator originating the operation, for potential rewards.
* 0 if the action is executed directly by the user, without any middle-man
* @param fromUnderlying bool
* - `true` if the msg.sender comes with underlying tokens (e.g. USDC)
* - `false` if the msg.sender comes already with aTokens (e.g. aUSDC)
* @return uint256 The amount of StaticAToken minted, static balance
**/
function deposit(
address recipient,
uint256 amount,
uint16 referralCode,
bool fromUnderlying
) external returns (uint256) {
return _deposit(msg.sender, recipient, amount, referralCode, fromUnderlying);
}
/**
* @dev Burns `amount` of static aToken, with recipient receiving the corresponding amount of `ASSET`
* @param recipient The address that will receive the amount of `ASSET` withdrawn from the Aave protocol
* @param amount The amount to withdraw, in static balance of StaticAToken
* @param toUnderlying bool
* - `true` for the recipient to get underlying tokens (e.g. USDC)
* - `false` for the recipient to get aTokens (e.g. aUSDC)
* @return amountToBurn: StaticATokens burnt, static balance
* @return amountToWithdraw: underlying/aToken send to `recipient`, dynamic balance
**/
function withdraw(
address recipient,
uint256 amount,
bool toUnderlying
) external returns (uint256, uint256) {
return _withdraw(msg.sender, recipient, amount, 0, toUnderlying);
}
/**
* @dev Burns `amount` of static aToken, with recipient receiving the corresponding amount of `ASSET`
* @param recipient The address that will receive the amount of `ASSET` withdrawn from the Aave protocol
* @param amount The amount to withdraw, in dynamic balance of aToken/underlying asset
* @param toUnderlying bool
* - `true` for the recipient to get underlying tokens (e.g. USDC)
* - `false` for the recipient to get aTokens (e.g. aUSDC)
* @return amountToBurn: StaticATokens burnt, static balance
* @return amountToWithdraw: underlying/aToken send to `recipient`, dynamic balance
**/
function withdrawDynamicAmount(
address recipient,
uint256 amount,
bool toUnderlying
) external returns (uint256, uint256) {
return _withdraw(msg.sender, recipient, 0, amount, toUnderlying);
}
/**
* @dev Implements the permit function as for
* https://github.com/ethereum/EIPs/blob/8a34d644aacf0f9f8f00815307fd7dd5da07655f/EIPS/eip-2612.md
* @param owner The owner of the funds
* @param spender The spender
* @param value The amount
* @param deadline The deadline timestamp, type(uint256).max for max deadline
* @param v Signature param
* @param s Signature param
* @param r Signature param
* @param chainId Passing the chainId in order to be fork-compatible
*/
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s,
uint256 chainId
) external {
require(owner != address(0), 'INVALID_OWNER');
//solium-disable-next-line
require(block.timestamp <= deadline, 'INVALID_EXPIRATION');
uint256 currentValidNonce = _nonces[owner];
bytes32 digest =
keccak256(
abi.encodePacked(
'\x19\x01',
getDomainSeparator(chainId),
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, currentValidNonce, deadline))
)
);
require(owner == ecrecover(digest, v, r, s), 'INVALID_SIGNATURE');
_nonces[owner] = currentValidNonce.add(1);
_approve(owner, spender, value);
}
/**
* @dev Allows to deposit on Aave via meta-transaction
* https://github.com/ethereum/EIPs/blob/8a34d644aacf0f9f8f00815307fd7dd5da07655f/EIPS/eip-2612.md
* @param depositor Address from which the funds to deposit are going to be pulled
* @param recipient Address that will receive the staticATokens, in the average case, same as the `depositor`
* @param value The amount to deposit
* @param referralCode Code used to register the integrator originating the operation, for potential rewards.
* 0 if the action is executed directly by the user, without any middle-man
* @param fromUnderlying bool
* - `true` if the msg.sender comes with underlying tokens (e.g. USDC)
* - `false` if the msg.sender comes already with aTokens (e.g. aUSDC)
* @param deadline The deadline timestamp, type(uint256).max for max deadline
* @param sigParams Signature params: v,r,s
* @param chainId Passing the chainId in order to be fork-compatible
* @return uint256 The amount of StaticAToken minted, static balance
*/
function metaDeposit(
address depositor,
address recipient,
uint256 value,
uint16 referralCode,
bool fromUnderlying,
uint256 deadline,
SignatureParams calldata sigParams,
uint256 chainId
) external returns (uint256) {
require(depositor != address(0), 'INVALID_DEPOSITOR');
//solium-disable-next-line
require(block.timestamp <= deadline, 'INVALID_EXPIRATION');
uint256 currentValidNonce = _nonces[depositor];
bytes32 digest =
keccak256(
abi.encodePacked(
'\x19\x01',
getDomainSeparator(chainId),
keccak256(
abi.encode(
METADEPOSIT_TYPEHASH,
depositor,
recipient,
value,
referralCode,
fromUnderlying,
currentValidNonce,
deadline
)
)
)
);
require(
depositor == ecrecover(digest, sigParams.v, sigParams.r, sigParams.s),
'INVALID_SIGNATURE'
);
_nonces[depositor] = currentValidNonce.add(1);
_deposit(depositor, recipient, value, referralCode, fromUnderlying);
}
/**
* @dev Allows to withdraw from Aave via meta-transaction
* https://github.com/ethereum/EIPs/blob/8a34d644aacf0f9f8f00815307fd7dd5da07655f/EIPS/eip-2612.md
* @param owner Address owning the staticATokens
* @param recipient Address that will receive the underlying withdrawn from Aave
* @param staticAmount The amount of staticAToken to withdraw. If > 0, `dynamicAmount` needs to be 0
* @param dynamicAmount The amount of underlying/aToken to withdraw. If > 0, `staticAmount` needs to be 0
* @param toUnderlying bool
* - `true` for the recipient to get underlying tokens (e.g. USDC)
* - `false` for the recipient to get aTokens (e.g. aUSDC)
* @param deadline The deadline timestamp, type(uint256).max for max deadline
* @param sigParams Signature params: v,r,s
* @param chainId Passing the chainId in order to be fork-compatible
* @return amountToBurn: StaticATokens burnt, static balance
* @return amountToWithdraw: underlying/aToken send to `recipient`, dynamic balance
*/
function metaWithdraw(
address owner,
address recipient,
uint256 staticAmount,
uint256 dynamicAmount,
bool toUnderlying,
uint256 deadline,
SignatureParams calldata sigParams,
uint256 chainId
) external returns (uint256, uint256) {
require(owner != address(0), 'INVALID_OWNER');
//solium-disable-next-line
require(block.timestamp <= deadline, 'INVALID_EXPIRATION');
uint256 currentValidNonce = _nonces[owner];
bytes32 digest =
keccak256(
abi.encodePacked(
'\x19\x01',
getDomainSeparator(chainId),
keccak256(
abi.encode(
METAWITHDRAWAL_TYPEHASH,
owner,
recipient,
staticAmount,
dynamicAmount,
toUnderlying,
currentValidNonce,
deadline
)
)
)
);
require(owner == ecrecover(digest, sigParams.v, sigParams.r, sigParams.s), 'INVALID_SIGNATURE');
_nonces[owner] = currentValidNonce.add(1);
return _withdraw(owner, recipient, staticAmount, dynamicAmount, toUnderlying);
}
/**
* @dev Utility method to get the current aToken balance of an user, from his staticAToken balance
* @param account The address of the user
* @return uint256 The aToken balance
**/
function dynamicBalanceOf(address account) external view returns (uint256) {
return _staticToDynamicAmount(balanceOf(account), rate());
}
/**
* @dev Converts a static amount (scaled balance on aToken) to the aToken/underlying value,
* using the current liquidity index on Aave
* @param amount The amount to convert from
* @return uint256 The dynamic amount
**/
function staticToDynamicAmount(uint256 amount) external view returns (uint256) {
return _staticToDynamicAmount(amount, rate());
}
/**
* @dev Converts an aToken or underlying amount to the what it is denominated on the aToken as
* scaled balance, function of the principal and the liquidity index
* @param amount The amount to convert from
* @return uint256 The static (scaled) amount
**/
function dynamicToStaticAmount(uint256 amount) external view returns (uint256) {
return _dynamicToStaticAmount(amount, rate());
}
/**
* @dev Returns the Aave liquidity index of the underlying aToken, denominated rate here
* as it can be considered as an ever-increasing exchange rate
* @return bytes32 The domain separator
**/
function rate() public view returns (uint256) {
return LENDING_POOL.getReserveNormalizedIncome(address(ASSET));
}
/**
* @dev Function to return a dynamic domain separator, in order to be compatible with forks changing chainId
* @param chainId The chain id
* @return bytes32 The domain separator
**/
function getDomainSeparator(uint256 chainId) public view returns (bytes32) {
return
keccak256(
abi.encode(
EIP712_DOMAIN,
keccak256(bytes(name())),
keccak256(EIP712_REVISION),
chainId,
address(this)
)
);
}
function _dynamicToStaticAmount(uint256 amount, uint256 rate) internal pure returns (uint256) {
return amount.rayDiv(rate);
}
function _staticToDynamicAmount(uint256 amount, uint256 rate) internal pure returns (uint256) {
return amount.rayMul(rate);
}
function _deposit(
address depositor,
address recipient,
uint256 amount,
uint16 referralCode,
bool fromUnderlying
) internal returns (uint256) {
require(recipient != address(0), 'INVALID_RECIPIENT');
_updateRewards();
if (fromUnderlying) {
ASSET.safeTransferFrom(depositor, address(this), amount);
LENDING_POOL.deposit(address(ASSET), amount, address(this), referralCode);
} else {
ATOKEN.safeTransferFrom(depositor, address(this), amount);
}
uint256 amountToMint = _dynamicToStaticAmount(amount, rate());
_mint(recipient, amountToMint);
return amountToMint;
}
function _withdraw(
address owner,
address recipient,
uint256 staticAmount,
uint256 dynamicAmount,
bool toUnderlying
) internal returns (uint256, uint256) {
require(recipient != address(0), 'INVALID_RECIPIENT');
require(staticAmount == 0 || dynamicAmount == 0, 'ONLY_ONE_AMOUNT_FORMAT_ALLOWED');
_updateRewards();
uint256 userBalance = balanceOf(owner);
uint256 amountToWithdraw;
uint256 amountToBurn;
uint256 currentRate = rate();
if (staticAmount > 0) {
amountToBurn = (staticAmount > userBalance) ? userBalance : staticAmount;
amountToWithdraw = (staticAmount > userBalance)
? _staticToDynamicAmount(userBalance, currentRate)
: _staticToDynamicAmount(staticAmount, currentRate);
} else {
uint256 dynamicUserBalance = _staticToDynamicAmount(userBalance, currentRate);
amountToWithdraw = (dynamicAmount > dynamicUserBalance) ? dynamicUserBalance : dynamicAmount;
amountToBurn = _dynamicToStaticAmount(amountToWithdraw, currentRate);
}
_burn(owner, amountToBurn);
if (toUnderlying) {
LENDING_POOL.withdraw(address(ASSET), amountToWithdraw, recipient);
} else {
ATOKEN.safeTransfer(recipient, amountToWithdraw);
}
return (amountToBurn, amountToWithdraw);
}
/**
* @dev Updates rewards for senders and receiver in a transfer (no mint or burn)
* @param from The address of the sender of tokens
* @param to The address of the receiver of tokens
* @param amount The amount of tokens to transfer in WAD
*/
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal override {
if (from != address(0)) {
_updateUser(from);
}
if (to != address(0)) {
_updateUser(to);
}
}
/**
* @dev Updates virtual internal accounting of rewards.
*/
function _updateRewards() internal {
if (block.number > lastRewardBlock) {
lastRewardBlock = block.number;
uint256 _supply = totalSupply();
if (_supply == 0) {
// No rewards can have accrued since last because there were no funds.
return;
}
address[] memory assets = new address[](1);
assets[0] = address(ATOKEN);
uint256 freshRewards = _incentivesController.getRewardsBalance(assets, address(this));
uint256 externalLifetimeRewards = lifeTimeRewardsClaimed.add(freshRewards);
uint256 diff = externalLifetimeRewards.sub(lifeTimeRewards).wadToRay();
rewardIndex = rewardIndex.add((diff).rayDivNoRounding(_supply.wadToRay()));
lifeTimeRewards = externalLifetimeRewards;
}
}
/**
* @dev Claims rewards from `_incentivesController` and updates internal accounting of rewards.
*/
function collectAndUpdateRewards() public {
if (block.number > lastRewardBlock) {
lastRewardBlock = block.number;
uint256 _supply = totalSupply();
address[] memory assets = new address[](1);
assets[0] = address(ATOKEN);
uint256 freshlyClaimed =
_incentivesController.claimRewards(assets, type(uint256).max, address(this));
uint256 externalLifetimeRewards = lifeTimeRewardsClaimed.add(freshlyClaimed);
uint256 diff = externalLifetimeRewards.sub(lifeTimeRewards).wadToRay();
if (_supply > 0 && diff > 0) {
rewardIndex = rewardIndex.add((diff).rayDivNoRounding(_supply.wadToRay()));
}
if (diff > 0) {
lifeTimeRewards = externalLifetimeRewards;
}
// Unsure if we can also move this in
lifeTimeRewardsClaimed = externalLifetimeRewards;
}
}
/**
* @dev Claim rewards for a user.
* @param user The address of the user to claim rewards for
* @param forceUpdate Flag to retrieve latest rewards from `_incentiveController`
*/
function claimRewards(address user, bool forceUpdate) public {
if (forceUpdate) {
collectAndUpdateRewards();
}
uint256 balance = balanceOf(user);
uint256 reward = _getClaimableRewards(user, balance, false);
uint256 totBal = IERC20(currentRewardToken).balanceOf(address(this));
if (reward > totBal) {
// Throw away excess unclaimed rewards
reward = totBal;
}
if (reward > 0) {
_unclaimedRewards[user] = 0;
_updateUserIndex(user);
IERC20(currentRewardToken).safeTransfer(user, reward);
}
}
/**
* @dev Update the rewardDebt for a user with balance as his balance
* @param user The user to update
*/
function _updateUserIndex(address user) internal {
_userIndex[user] = rewardIndex;
}
/**
* @dev Adding the pending rewards to the unclaimed for specific user and updating user index
* @param user The address of the user to update
*/
function _updateUser(address user) internal {
uint256 balance = balanceOf(user);
if (balance > 0) {
uint256 pending = _getPendingRewards(user, balance, false);
_unclaimedRewards[user] = _unclaimedRewards[user].add(pending);
}
_updateUserIndex(user);
}
/**
* @dev Compute the pending in RAY (rounded down). Pending is the amount to add (not yet unclaimed) rewards in RAY (rounded down).
* @param user The user to compute for
* @param balance The balance of the user
* @param fresh Flag to account for rewards not claimed by contract yet
* @return The amound of pending rewards in RAY
*/
function _getPendingRewards(
address user,
uint256 balance,
bool fresh
) internal view returns (uint256) {
if (balance == 0) {
return 0;
}
uint256 rayBalance = balance.wadToRay();
uint256 _supply = totalSupply();
uint256 _rewardIndex = rewardIndex;
if (_supply != 0 && fresh) {
address[] memory assets = new address[](1);
assets[0] = address(ATOKEN);
uint256 freshReward = _incentivesController.getRewardsBalance(assets, address(this));
uint256 externalLifetimeRewards = lifeTimeRewardsClaimed.add(freshReward);
uint256 diff = externalLifetimeRewards.sub(lifeTimeRewards).wadToRay();
_rewardIndex = _rewardIndex.add((diff).rayDivNoRounding(_supply.wadToRay()));
}
return rayBalance.rayMulNoRounding(_rewardIndex.sub(_userIndex[user]));
}
/**
* @dev Compute the claimable rewards for a user
* @param user The address of the user
* @param balance The balance of the user in WAD
* @param fresh Flag to account for rewards not claimed by contract yet
* @return The total rewards that can be claimed by the user (if `fresh` flag true, after updating rewards)
*/
function _getClaimableRewards(
address user,
uint256 balance,
bool fresh
) internal view returns (uint256) {
uint256 reward = _unclaimedRewards[user].add(_getPendingRewards(user, balance, fresh));
return reward.rayToWadNoRounding();
}
/**
* @dev Get the total claimable rewards of the contract.
* @return The current balance + pending rewards from the `_incentivesController`
*/
function getTotalClaimableRewards() external view returns (uint256) {
address[] memory assets = new address[](1);
assets[0] = address(ATOKEN);
uint256 freshRewards = _incentivesController.getRewardsBalance(assets, address(this));
return IERC20(currentRewardToken).balanceOf(address(this)).add(freshRewards);
}
/**
* @dev Get the total claimable rewards for a user in WAD
* @param user The address of the user
* @return The claimable amount of rewards in WAD
*/
function getClaimableRewards(address user) external view returns (uint256) {
return _getClaimableRewards(user, balanceOf(user), true);
}
/**
* @dev The unclaimed rewards for a user in WAD
* @param user The address of the user
* @return The unclaimed amount of rewards in WAD
*/
function getUnclaimedRewards(address user) external view returns (uint256) {
return _unclaimedRewards[user].rayToWadNoRounding();
}
}