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.
- Loading branch information
Showing
23 changed files
with
5,308 additions
and
2 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
// 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, IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; | ||
|
||
import "../utils/UUPSHelper.sol"; | ||
|
||
interface IDistributionCreator { | ||
function distributor() external view returns (address); | ||
function feeRecipient() external view returns (address); | ||
} | ||
|
||
abstract contract BaseMerklTokenWrapper is UUPSHelper, ERC20Upgradeable { | ||
using SafeERC20 for IERC20; | ||
|
||
// ================================= CONSTANTS ================================= | ||
|
||
IDistributionCreator public constant DISTRIBUTOR_CREATOR = | ||
IDistributionCreator(0x8BB4C975Ff3c250e0ceEA271728547f3802B36Fd); | ||
|
||
address public immutable DISTRIBUTOR = DISTRIBUTOR_CREATOR.distributor(); | ||
address public immutable FEE_RECIPIENT = DISTRIBUTOR_CREATOR.feeRecipient(); | ||
|
||
// ================================= VARIABLES ================================= | ||
|
||
/// @notice `Core` contract handling access control | ||
ICore public core; | ||
|
||
// =================================== 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 token() public view virtual returns (address); | ||
|
||
function isTokenWrapper() external pure returns (bool) { | ||
return true; | ||
} | ||
|
||
function initialize(ICore _core) public initializer onlyProxy { | ||
__ERC20_init( | ||
string.concat("Merkl Token Wrapper - ", IERC20Metadata(token()).name()), | ||
string.concat("mtw", IERC20Metadata(token()).symbol()) | ||
); | ||
__UUPSUpgradeable_init(); | ||
if (address(_core) == address(0)) revert ZeroAddress(); | ||
core = _core; | ||
} | ||
|
||
/// @notice Recovers any ERC20 token | ||
/// @dev Governance only, to trigger only if something went wrong | ||
function recoverERC20(address tokenAddress, address to, uint256 amountToRecover) external onlyGovernor { | ||
IERC20(tokenAddress).safeTransfer(to, amountToRecover); | ||
emit Recovered(tokenAddress, to, amountToRecover); | ||
} | ||
|
||
/// @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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
// 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, IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; | ||
|
||
import { BaseMerklTokenWrapper } from "./BaseTokenWrapper.sol"; | ||
|
||
interface IVesting { | ||
function rdntToken() external view returns (address); | ||
function vestTokens(address, uint256, bool) external returns (address); | ||
} | ||
|
||
/// @title Radiant MTW | ||
/// @dev This token can only be held by merkl distributor | ||
/// @dev Transferring to the distributor will require transferring the underlying token to this contract | ||
/// @dev Transferring from the distributor will trigger vesting action | ||
/// @dev Transferring token to the distributor is permissionless so anyone could mint this wrapper - the only | ||
/// impact would be to forfeit these tokens | ||
contract RadiantMerklTokenWrapper is BaseMerklTokenWrapper { | ||
using SafeERC20 for IERC20; | ||
|
||
// ================================= CONSTANTS ================================= | ||
|
||
IVesting public constant VESTING = IVesting(0x76ba3eC5f5adBf1C58c91e86502232317EeA72dE); | ||
address internal immutable _UNDERLYING = VESTING.rdntToken(); | ||
|
||
// ================================= FUNCTIONS ================================= | ||
|
||
function token() public view override returns (address) { | ||
return _UNDERLYING; | ||
} | ||
|
||
function _beforeTokenTransfer(address from, address to, uint256 amount) internal override { | ||
// Needs an RDNT approval beforehand, this is how mints of coupons are done | ||
if (to == DISTRIBUTOR) { | ||
IERC20(_UNDERLYING).safeTransferFrom(from, address(this), amount); | ||
_mint(from, amount); // These are then transferred to the distributor | ||
} | ||
|
||
// Will be burn right after, to avoid having any token aside from on the distributor | ||
if (to == FEE_RECIPIENT) { | ||
IERC20(_UNDERLYING).safeTransferFrom(from, FEE_RECIPIENT, amount); | ||
_mint(from, amount); // These are then transferred to the fee manager | ||
} | ||
} | ||
|
||
function _afterTokenTransfer(address from, address to, uint256 amount) internal override { | ||
if (to == FEE_RECIPIENT) { | ||
_burn(to, amount); // To avoid having any token aside from on the distributor | ||
} | ||
|
||
if (from == DISTRIBUTOR) { | ||
_burn(to, amount); | ||
|
||
// Vesting logic | ||
IERC20(_UNDERLYING).transfer(address(VESTING), amount); | ||
VESTING.vestTokens(to, amount, true); | ||
} | ||
} | ||
} |
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); | ||
} | ||
} |
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,44 @@ | ||
import { DistributionCreator__factory } from '@angleprotocol/sdk'; | ||
import { DeployFunction } from 'hardhat-deploy/types'; | ||
import yargs from 'yargs'; | ||
import { Distributor__factory } from '../typechain'; | ||
|
||
const argv = yargs.env('').boolean('ci').parseSync(); | ||
|
||
const func: DeployFunction = async ({ deployments, ethers, network }) => { | ||
const { deploy } = deployments; | ||
const { deployer } = await ethers.getNamedSigners(); | ||
|
||
const couponName = 'RadiantMerklTokenWrapper'; | ||
const distributionCreator = DistributionCreator__factory.connect('0x8BB4C975Ff3c250e0ceEA271728547f3802B36Fd', deployer) | ||
const core = await distributionCreator.core() | ||
|
||
console.log(`Deploying Coupon`); | ||
console.log('Starting with the implementation'); | ||
|
||
await deploy(`${couponName}_Implementation`, { | ||
contract: couponName, | ||
from: deployer.address, | ||
args: [], | ||
log: !argv.ci, | ||
}); | ||
|
||
const implementationAddress = (await ethers.getContract(`${couponName}_Implementation`)).address; | ||
|
||
console.log('Starting with the implementation'); | ||
|
||
const distributorInterface = Distributor__factory.createInterface(); | ||
|
||
await deploy(`${couponName}_Proxy`, { | ||
contract: 'ERC1967Proxy', | ||
from: deployer.address, | ||
args: [implementationAddress, distributorInterface.encodeFunctionData('initialize', [core])], | ||
log: !argv.ci, | ||
}); | ||
|
||
console.log(`Successfully deployed the contract ${couponName} at ${implementationAddress}`); | ||
console.log(''); | ||
}; | ||
|
||
func.tags = ['mtw']; | ||
export default func; |
Oops, something went wrong.