generated from AngleProtocol/boilerplate
-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* v1 of Merkl contract * feat: poc for contract * remove feeManager * feat: cooldown contract * feat: handle fee recipient
- Loading branch information
Showing
3 changed files
with
217 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
|
||
pragma solidity ^0.8.17; | ||
|
||
import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; | ||
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; | ||
import "../DistributionCreator.sol"; | ||
|
||
import "../utils/UUPSHelper.sol"; | ||
|
||
contract AaveTokenWrapper is UUPSHelper, ERC20Upgradeable { | ||
using SafeERC20 for IERC20; | ||
|
||
// ================================= VARIABLES ================================= | ||
|
||
/// @notice `Core` contract handling access control | ||
ICore public core; | ||
|
||
// could be put as immutable in non upgradeable contract | ||
address public token; | ||
address public distributor; | ||
address public distributionCreator; | ||
|
||
mapping(address => uint256) public isMasterClaimer; | ||
mapping(address => address) public delegateReceiver; | ||
mapping(address => uint256) public permissionlessClaim; | ||
|
||
error InvalidClaim(); | ||
|
||
// =================================== EVENTS ================================== | ||
|
||
event Recovered(address indexed token, address indexed to, uint256 amount); | ||
|
||
// ================================= MODIFIERS ================================= | ||
|
||
/// @notice Checks whether the `msg.sender` has the governor role or the guardian role | ||
modifier onlyGovernor() { | ||
if (!core.isGovernor(msg.sender)) revert NotGovernor(); | ||
_; | ||
} | ||
|
||
// ================================= FUNCTIONS ================================= | ||
|
||
function initialize( | ||
address underlyingToken, | ||
address _distributor, | ||
address _core, | ||
address _distributionCreator | ||
) public initializer { | ||
// TODO could fetch name and symbol based on real token | ||
__ERC20_init("AaveTokenWrapper", "ATW"); | ||
__UUPSUpgradeable_init(); | ||
if (underlyingToken == address(0) || _distributor == address(0) || _distributionCreator == address(0)) | ||
revert ZeroAddress(); | ||
ICore(_core).isGovernor(msg.sender); | ||
token = underlyingToken; | ||
distributor = _distributor; | ||
distributionCreator = _distributionCreator; | ||
core = ICore(_core); | ||
} | ||
|
||
function _beforeTokenTransfer(address from, address to, uint256 amount) internal override { | ||
// Needs an approval before hand, this is how mints are done | ||
if (to == distributor) { | ||
IERC20(token).safeTransferFrom(from, address(this), amount); | ||
_mint(from, amount); // These are then transfered to the distributor | ||
} else { | ||
if (to == _getFeeRecipient()) { | ||
IERC20(token).safeTransferFrom(from, to, amount); | ||
_mint(from, amount); | ||
} | ||
} | ||
} | ||
|
||
function _afterTokenTransfer(address from, address to, uint256 amount) internal override { | ||
if (from == address(distributor)) { | ||
if (tx.origin == to || permissionlessClaim[to] == 1 || isMasterClaimer[tx.origin] == 1) { | ||
_handleClaim(to, amount); | ||
} else if (allowance(to, tx.origin) > amount) { | ||
_spendAllowance(to, tx.origin, amount); | ||
_handleClaim(to, amount); | ||
} else { | ||
revert InvalidClaim(); | ||
} | ||
} else if (to == _getFeeRecipient()) { | ||
// To avoid having any token aside from the distributor | ||
_burn(to, amount); | ||
} | ||
} | ||
|
||
function _handleClaim(address to, uint256 amount) internal { | ||
address delegate = delegateReceiver[to]; | ||
_burn(to, amount); | ||
if (delegate == address(0) || delegate == to) { | ||
IERC20(token).safeTransfer(to, amount); | ||
} else { | ||
IERC20(token).safeTransfer(delegate, amount); | ||
} | ||
} | ||
|
||
function _getFeeRecipient() internal view returns (address feeRecipient) { | ||
address _distributionCreator = distributionCreator; | ||
feeRecipient = DistributionCreator(_distributionCreator).feeRecipient(); | ||
feeRecipient = feeRecipient == address(0) ? _distributionCreator : feeRecipient; | ||
} | ||
|
||
/// @notice Recovers any ERC20 token | ||
function recoverERC20(address tokenAddress, address to, uint256 amountToRecover) external onlyGovernor { | ||
IERC20(tokenAddress).safeTransfer(to, amountToRecover); | ||
emit Recovered(tokenAddress, to, amountToRecover); | ||
} | ||
|
||
function toggleMasterClaimer(address claimer) external onlyGovernor { | ||
uint256 claimStatus = 1 - isMasterClaimer[claimer]; | ||
isMasterClaimer[claimer] = claimStatus; | ||
} | ||
|
||
function togglePermissionlessClaim() external { | ||
uint256 permission = 1 - permissionlessClaim[msg.sender]; | ||
permissionlessClaim[msg.sender] = permission; | ||
} | ||
|
||
function updateDelegateReceiver(address receiver) external { | ||
delegateReceiver[msg.sender] = receiver; | ||
} | ||
|
||
/// @inheritdoc UUPSUpgradeable | ||
function _authorizeUpgrade(address) internal view override onlyGovernorUpgrader(core) {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
|
||
pragma solidity ^0.8.17; | ||
|
||
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; | ||
import { ERC4626, ERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; | ||
|
||
// Cooldown logic forked from: https://github.com/aave/aave-stake-v2/blob/master/contracts/stake/StakedTokenV3.sol | ||
contract StakedToken is ERC4626 { | ||
uint256 public immutable COOLDOWN_SECONDS; | ||
uint256 public immutable UNSTAKE_WINDOW; | ||
|
||
mapping(address => uint256) public stakerCooldown; | ||
|
||
error InsufficientCooldown(); | ||
error InvalidBalanceOnCooldown(); | ||
error UnstakeWindowFinished(); | ||
|
||
event Cooldown(address indexed sender, uint256 timestamp); | ||
|
||
// ================================= FUNCTIONS ================================= | ||
|
||
constructor( | ||
IERC20 asset_, | ||
string memory name_, | ||
string memory symbol_, | ||
uint256 cooldownSeconds, | ||
uint256 unstakeWindow | ||
) ERC4626(asset_) ERC20(name_, symbol_) { | ||
COOLDOWN_SECONDS = cooldownSeconds; | ||
UNSTAKE_WINDOW = unstakeWindow; | ||
} | ||
|
||
function _beforeTokenTransfer(address from, address to, uint256 amount) internal override { | ||
if (from == address(0)) { | ||
// For a mint: we update the cooldown of the receiver if needed | ||
stakerCooldown[to] = getNextCooldownTimestamp(0, amount, to, balanceOf(to)); | ||
} else if (to == address(0)) { | ||
uint256 cooldownEndTimestamp = stakerCooldown[from] + COOLDOWN_SECONDS; | ||
if (block.timestamp > cooldownEndTimestamp) revert InsufficientCooldown(); | ||
if (block.timestamp - cooldownEndTimestamp <= UNSTAKE_WINDOW) revert UnstakeWindowFinished(); | ||
} else if (from != to) { | ||
uint256 previousSenderCooldown = stakerCooldown[from]; | ||
stakerCooldown[to] = getNextCooldownTimestamp(previousSenderCooldown, amount, to, balanceOf(to)); | ||
// if cooldown was set and whole balance of sender was transferred - clear cooldown | ||
if (balanceOf(from) == amount && previousSenderCooldown != 0) { | ||
stakerCooldown[from] = 0; | ||
} | ||
} | ||
} | ||
|
||
function getNextCooldownTimestamp( | ||
uint256 fromCooldownTimestamp, | ||
uint256 amountToReceive, | ||
address toAddress, | ||
uint256 toBalance | ||
) public view returns (uint256 toCooldownTimestamp) { | ||
toCooldownTimestamp = stakerCooldown[toAddress]; | ||
if (toCooldownTimestamp == 0) return 0; | ||
|
||
uint256 minimalValidCooldownTimestamp = block.timestamp - COOLDOWN_SECONDS - UNSTAKE_WINDOW; | ||
|
||
if (minimalValidCooldownTimestamp > toCooldownTimestamp) { | ||
toCooldownTimestamp = 0; | ||
} else { | ||
fromCooldownTimestamp = (minimalValidCooldownTimestamp > fromCooldownTimestamp) | ||
? block.timestamp | ||
: fromCooldownTimestamp; | ||
|
||
if (fromCooldownTimestamp >= toCooldownTimestamp) { | ||
toCooldownTimestamp = | ||
(amountToReceive * fromCooldownTimestamp + toBalance * toCooldownTimestamp) / | ||
(amountToReceive + toBalance); | ||
} | ||
} | ||
} | ||
|
||
function cooldown() external { | ||
if (balanceOf(msg.sender) != 0) revert InvalidBalanceOnCooldown(); | ||
stakerCooldown[msg.sender] = block.timestamp; | ||
emit Cooldown(msg.sender, block.timestamp); | ||
} | ||
} |