diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cf4807bc..21ad5ca1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 19.3b0 + rev: 24.3.0 hooks: - id: black - repo: https://gitlab.com/pycqa/flake8 @@ -11,6 +11,12 @@ repos: rev: 5.7.0 hooks: - id: isort - +- repo: https://github.com/VersoriumX/EthereumX + rev: 19.0.0 + hooks: + - id: EthereumX default_language_version: python: python3.8 + + + diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..034e8480 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. diff --git a/contracts/pool-templates/a/pooldata.json b/contracts/pool-templates/a/pooldata.json index c3ec9454..bf2f8ee0 100644 --- a/contracts/pool-templates/a/pooldata.json +++ b/contracts/pool-templates/a/pooldata.json @@ -1,5 +1,7 @@ { - "wrapped_contract": "ATokenMock", + "wrapped_contract": "EthereumX", + "contract_address": "0x8487B97c91ecC1a03b4907B64Bdeab306B888c0E", + "constructor_owner": "0x608cfC1575b56a82a352f14d61be100FA9709D75", "pool_types": ["arate"], "coins": [ { @@ -12,7 +14,7 @@ "decimals": 18, "wrapped_decimals": 18, "wrapped": true, - "tethered": false + "tethered": true } ] } diff --git a/contracts/pools/ETHX/LiquidityGauge.vy b/contracts/pools/ETHX/LiquidityGauge.vy new file mode 100644 index 00000000..17eb35e9 --- /dev/null +++ b/contracts/pools/ETHX/LiquidityGauge.vy @@ -0,0 +1,758 @@ +# @version 0.2.8 +""" +@title Liquidity Gauge v2 +@author Curve Finance +@license MIT +""" + +from vyper.interfaces import ERC20 + +implements: ERC20 + + +interface CRV20: + def future_epoch_time_write() -> uint256: nonpayable + def rate() -> uint256: view + +interface Controller: + def period() -> int128: view + def period_write() -> int128: nonpayable + def period_timestamp(p: int128) -> uint256: view + def gauge_relative_weight(addr: address, time: uint256) -> uint256: view + def voting_escrow() -> address: view + def checkpoint(): nonpayable + def checkpoint_gauge(addr: address): nonpayable + +interface Minter: + def token() -> address: view + def controller() -> address: view + def minted(user: address, gauge: address) -> uint256: view + +interface VotingEscrow: + def user_point_epoch(addr: address) -> uint256: view + def user_point_history__ts(addr: address, epoch: uint256) -> uint256: view + +interface ERC20Extended: + def symbol() -> String[26]: view + + +event Deposit: + provider: indexed(address) + value: uint256 + +event Withdraw: + provider: indexed(address) + value: uint256 + +event UpdateLiquidityLimit: + user: address + original_balance: uint256 + original_supply: uint256 + working_balance: uint256 + working_supply: uint256 + +event CommitOwnership: + admin: address + +event ApplyOwnership: + admin: address + +event Transfer: + _from: indexed(address) + _to: indexed(address) + _value: uint256 + +event Approval: + _owner: indexed(address) + _spender: indexed(address) + _value: uint256 + + +MAX_REWARDS: constant(uint256) = 8 +TOKENLESS_PRODUCTION: constant(uint256) = 40 +WEEK: constant(uint256) = 604800 + +minter: public(address) +crv_token: public(address) +lp_token: public(address) +controller: public(address) +voting_escrow: public(address) +future_epoch_time: public(uint256) + +balanceOf: public(HashMap[address, uint256]) +totalSupply: public(uint256) +allowances: HashMap[address, HashMap[address, uint256]] + +name: public(String[64]) +symbol: public(String[32]) + +# caller -> recipient -> can deposit? +approved_to_deposit: public(HashMap[address, HashMap[address, bool]]) + +working_balances: public(HashMap[address, uint256]) +working_supply: public(uint256) + +# The goal is to be able to calculate ∫(rate * balance / totalSupply dt) from 0 till checkpoint +# All values are kept in units of being multiplied by 1e18 +period: public(int128) +period_timestamp: public(uint256[100000000000000000000000000000]) + +# 1e18 * ∫(rate(t) / totalSupply(t) dt) from 0 till checkpoint +integrate_inv_supply: public(uint256[100000000000000000000000000000]) # bump epoch when rate() changes + +# 1e18 * ∫(rate(t) / totalSupply(t) dt) from (last_action) till checkpoint +integrate_inv_supply_of: public(HashMap[address, uint256]) +integrate_checkpoint_of: public(HashMap[address, uint256]) + +# ∫(balance * rate(t) / totalSupply(t) dt) from 0 till checkpoint +# Units: rate * t = already number of coins per address to issue +integrate_fraction: public(HashMap[address, uint256]) + +inflation_rate: public(uint256) + +# For tracking external rewards +reward_contract: public(address) +reward_tokens: public(address[MAX_REWARDS]) + +# deposit / withdraw / claim +reward_sigs: bytes32 + +# reward token -> integral +reward_integral: public(HashMap[address, uint256]) + +# reward token -> claiming address -> integral +reward_integral_for: public(HashMap[address, HashMap[address, uint256]]) + +admin: public(address) +future_admin: public(address) # Can and will be a smart contract +is_killed: public(bool) + + +@external +def __init__(_lp_token: address, _minter: address, _admin: address): + """ + @notice Contract constructor + @param _lp_token Liquidity Pool contract address + @param _minter Minter contract address + @param _admin Admin who can kill the gauge + """ + + symbol: String[26] = ERC20Extended(_lp_token).symbol() + self.name = concat("Curve.fi ", symbol, " Gauge Deposit") + self.symbol = concat(symbol, "-gauge") + + crv_token: address = Minter(_minter).token() + controller: address = Minter(_minter).controller() + + self.lp_token = _lp_token + self.minter = _minter + self.admin = _admin + self.crv_token = crv_token + self.controller = controller + self.voting_escrow = Controller(controller).voting_escrow() + + self.period_timestamp[0] = block.timestamp + self.inflation_rate = CRV20(crv_token).rate() + self.future_epoch_time = CRV20(crv_token).future_epoch_time_write() + + +@view +@external +def decimals() -> uint256: + """ + @notice Get the number of decimals for this token + @dev Implemented as a view method to reduce gas costs + @return uint256 decimal places + """ + return 18 + + +@view +@external +def integrate_checkpoint() -> uint256: + return self.period_timestamp[self.period] + + +@internal +def _update_liquidity_limit(addr: address, l: uint256, L: uint256): + """ + @notice Calculate limits which depend on the amount of CRV token per-user. + Effectively it calculates working balances to apply amplification + of CRV production by CRV + @param addr User address + @param l User's amount of liquidity (LP tokens) + @param L Total amount of liquidity (LP tokens) + """ + # To be called after totalSupply is updated + _voting_escrow: address = self.voting_escrow + voting_balance: uint256 = ERC20(_voting_escrow).balanceOf(addr) + voting_total: uint256 = ERC20(_voting_escrow).totalSupply() + + lim: uint256 = l * TOKENLESS_PRODUCTION / 100 + if voting_total > 0: + lim += L * voting_balance / voting_total * (100 - TOKENLESS_PRODUCTION) / 100 + + lim = min(l, lim) + old_bal: uint256 = self.working_balances[addr] + self.working_balances[addr] = lim + _working_supply: uint256 = self.working_supply + lim - old_bal + self.working_supply = _working_supply + + log UpdateLiquidityLimit(addr, l, L, lim, _working_supply) + + +@internal +def _checkpoint_rewards(_addr: address, _total_supply: uint256): + """ + @notice Claim pending rewards and checkpoint rewards for a user + """ + if _total_supply == 0: + return + + reward_balances: uint256[MAX_REWARDS] = empty(uint256[MAX_REWARDS]) + reward_tokens: address[MAX_REWARDS] = empty(address[MAX_REWARDS]) + for i in range(MAX_REWARDS): + token: address = self.reward_tokens[i] + if token == ZERO_ADDRESS: + break + reward_tokens[i] = token + reward_balances[i] = ERC20(token).balanceOf(self) + + # claim from reward contract + raw_call(self.reward_contract, slice(self.reward_sigs, 8, 4)) # dev: bad claim sig + + user_balance: uint256 = self.balanceOf[_addr] + for i in range(MAX_REWARDS): + token: address = reward_tokens[i] + if token == ZERO_ADDRESS: + break + dI: uint256 = 10**18 * (ERC20(token).balanceOf(self) - reward_balances[i]) / _total_supply + if _addr == ZERO_ADDRESS: + if dI != 0: + self.reward_integral[token] += dI + continue + + integral: uint256 = self.reward_integral[token] + dI + if dI != 0: + self.reward_integral[token] = integral + + integral_for: uint256 = self.reward_integral_for[token][_addr] + if integral_for < integral: + claimable: uint256 = user_balance * (integral - integral_for) / 10**18 + self.reward_integral_for[token][_addr] = integral + if claimable != 0: + response: Bytes[32] = raw_call( + token, + concat( + method_id("transfer(address,uint256)"), + convert(_addr, bytes32), + convert(claimable, bytes32), + ), + max_outsize=32, + ) + if len(response) != 0: + assert convert(response, bool) + + +@internal +def _checkpoint(addr: address): + """ + @notice Checkpoint for a user + @param addr User address + """ + _period: int128 = self.period + _period_time: uint256 = self.period_timestamp[_period] + _integrate_inv_supply: uint256 = self.integrate_inv_supply[_period] + rate: uint256 = self.inflation_rate + new_rate: uint256 = rate + prev_future_epoch: uint256 = self.future_epoch_time + if prev_future_epoch >= _period_time: + _token: address = self.crv_token + self.future_epoch_time = CRV20(_token).future_epoch_time_write() + new_rate = CRV20(_token).rate() + self.inflation_rate = new_rate + + if self.is_killed: + # Stop distributing inflation as soon as killed + rate = 0 + + # Update integral of 1/supply + if block.timestamp > _period_time: + _working_supply: uint256 = self.working_supply + _controller: address = self.controller + Controller(_controller).checkpoint_gauge(self) + prev_week_time: uint256 = _period_time + week_time: uint256 = min((_period_time + WEEK) / WEEK * WEEK, block.timestamp) + + for i in range(500): + dt: uint256 = week_time - prev_week_time + w: uint256 = Controller(_controller).gauge_relative_weight(self, prev_week_time / WEEK * WEEK) + + if _working_supply > 0: + if prev_future_epoch >= prev_week_time and prev_future_epoch < week_time: + # If we went across one or multiple epochs, apply the rate + # of the first epoch until it ends, and then the rate of + # the last epoch. + # If more than one epoch is crossed - the gauge gets less, + # but that'd meen it wasn't called for more than 1 year + _integrate_inv_supply += rate * w * (prev_future_epoch - prev_week_time) / _working_supply + rate = new_rate + _integrate_inv_supply += rate * w * (week_time - prev_future_epoch) / _working_supply + else: + _integrate_inv_supply += rate * w * dt / _working_supply + # On precisions of the calculation + # rate ~= 10e18 + # last_weight > 0.01 * 1e18 = 1e16 (if pool weight is 1%) + # _working_supply ~= TVL * 1e18 ~= 1e26 ($100M for example) + # The largest loss is at dt = 1 + # Loss is 1e-9 - acceptable + + if week_time == block.timestamp: + break + prev_week_time = week_time + week_time = min(week_time + WEEK, block.timestamp) + + _period += 1 + self.period = _period + self.period_timestamp[_period] = block.timestamp + self.integrate_inv_supply[_period] = _integrate_inv_supply + + # Update user-specific integrals + _working_balance: uint256 = self.working_balances[addr] + self.integrate_fraction[addr] += _working_balance * (_integrate_inv_supply - self.integrate_inv_supply_of[addr]) / 10 ** 18 + self.integrate_inv_supply_of[addr] = _integrate_inv_supply + self.integrate_checkpoint_of[addr] = block.timestamp + + +@external +def user_checkpoint(addr: address) -> bool: + """ + @notice Record a checkpoint for `addr` + @param addr User address + @return bool success + """ + assert (msg.sender == addr) or (msg.sender == self.minter) # dev: unauthorized + self._checkpoint(addr) + self._update_liquidity_limit(addr, self.balanceOf[addr], self.totalSupply) + return True + + +@external +def claimable_tokens(addr: address) -> uint256: + """ + @notice Get the number of claimable tokens per user + @dev This function should be manually changed to "view" in the ABI + @return uint256 number of claimable tokens per user + """ + self._checkpoint(addr) + return self.integrate_fraction[addr] - Minter(self.minter).minted(addr, self) + + +@external +@nonreentrant('lock') +def claimable_reward(_addr: address, _token: address) -> uint256: + """ + @notice Get the number of claimable reward tokens for a user + @dev This function should be manually changed to "view" in the ABI + Calling it via a transaction will claim available reward tokens + @param _addr Account to get reward amount for + @param _token Token to get reward amount for + @return uint256 Claimable reward token amount + """ + claimable: uint256 = ERC20(_token).balanceOf(_addr) + if self.reward_contract != ZERO_ADDRESS: + self._checkpoint_rewards(_addr, self.totalSupply) + claimable = ERC20(_token).balanceOf(_addr) - claimable + + integral: uint256 = self.reward_integral[_token] + integral_for: uint256 = self.reward_integral_for[_token][_addr] + + if integral_for < integral: + claimable += self.balanceOf[_addr] * (integral - integral_for) / 10**18 + + return claimable + + +@external +@nonreentrant('lock') +def claim_rewards(_addr: address = msg.sender): + """ + @notice Claim available reward tokens for `_addr` + @param _addr Address to claim for + """ + self._checkpoint_rewards(_addr, self.totalSupply) + + +@external +@nonreentrant('lock') +def claim_historic_rewards(_reward_tokens: address[MAX_REWARDS], _addr: address = msg.sender): + """ + @notice Claim reward tokens available from a previously-set staking contract + @param _reward_tokens Array of reward token addresses to claim + @param _addr Address to claim for + """ + for token in _reward_tokens: + if token == ZERO_ADDRESS: + break + integral: uint256 = self.reward_integral[token] + integral_for: uint256 = self.reward_integral_for[token][_addr] + + if integral_for < integral: + claimable: uint256 = self.balanceOf[_addr] * (integral - integral_for) / 10**18 + self.reward_integral_for[token][_addr] = integral + response: Bytes[32] = raw_call( + token, + concat( + method_id("transfer(address,uint256)"), + convert(_addr, bytes32), + convert(claimable, bytes32), + ), + max_outsize=32, + ) + if len(response) != 0: + assert convert(response, bool) + + +@external +def kick(addr: address): + """ + @notice Kick `addr` for abusing their boost + @dev Only if either they had another voting event, or their voting escrow lock expired + @param addr Address to kick + """ + _voting_escrow: address = self.voting_escrow + t_last: uint256 = self.integrate_checkpoint_of[addr] + t_ve: uint256 = VotingEscrow(_voting_escrow).user_point_history__ts( + addr, VotingEscrow(_voting_escrow).user_point_epoch(addr) + ) + _balance: uint256 = self.balanceOf[addr] + + assert ERC20(self.voting_escrow).balanceOf(addr) == 0 or t_ve > t_last # dev: kick not allowed + assert self.working_balances[addr] > _balance * TOKENLESS_PRODUCTION / 100 # dev: kick not needed + + self._checkpoint(addr) + self._update_liquidity_limit(addr, self.balanceOf[addr], self.totalSupply) + + +@external +def set_approve_deposit(addr: address, can_deposit: bool): + """ + @notice Set whether `addr` can deposit tokens for `msg.sender` + @param addr Address to set approval on + @param can_deposit bool - can this account deposit for `msg.sender`? + """ + self.approved_to_deposit[addr][msg.sender] = can_deposit + + +@external +@nonreentrant('lock') +def deposit(_value: uint256, _addr: address = msg.sender): + """ + @notice Deposit `_value` LP tokens + @dev Depositting also claims pending reward tokens + @param _value Number of tokens to deposit + @param _addr Address to deposit for + """ + if _addr != msg.sender: + assert self.approved_to_deposit[msg.sender][_addr], "Not approved" + + self._checkpoint(_addr) + + if _value != 0: + reward_contract: address = self.reward_contract + total_supply: uint256 = self.totalSupply + if reward_contract != ZERO_ADDRESS: + self._checkpoint_rewards(_addr, total_supply) + + total_supply += _value + new_balance: uint256 = self.balanceOf[_addr] + _value + self.balanceOf[_addr] = new_balance + self.totalSupply = total_supply + + self._update_liquidity_limit(_addr, new_balance, total_supply) + + ERC20(self.lp_token).transferFrom(msg.sender, self, _value) + if reward_contract != ZERO_ADDRESS: + deposit_sig: Bytes[4] = slice(self.reward_sigs, 0, 4) + if convert(deposit_sig, uint256) != 0: + raw_call( + reward_contract, + concat(deposit_sig, convert(_value, bytes32)) + ) + + log Deposit(_addr, _value) + log Transfer(ZERO_ADDRESS, _addr, _value) + + +@external +@nonreentrant('lock') +def withdraw(_value: uint256): + """ + @notice Withdraw `_value` LP tokens + @dev Withdrawing also claims pending reward tokens + @param _value Number of tokens to withdraw + """ + self._checkpoint(msg.sender) + + if _value != 0: + reward_contract: address = self.reward_contract + total_supply: uint256 = self.totalSupply + if reward_contract != ZERO_ADDRESS: + self._checkpoint_rewards(msg.sender, total_supply) + + total_supply -= _value + new_balance: uint256 = self.balanceOf[msg.sender] - _value + self.balanceOf[msg.sender] = new_balance + self.totalSupply = total_supply + + self._update_liquidity_limit(msg.sender, new_balance, total_supply) + + if reward_contract != ZERO_ADDRESS: + withdraw_sig: Bytes[4] = slice(self.reward_sigs, 4, 4) + if convert(withdraw_sig, uint256) != 0: + raw_call( + reward_contract, + concat(withdraw_sig, convert(_value, bytes32)) + ) + ERC20(self.lp_token).transfer(msg.sender, _value) + + log Withdraw(msg.sender, _value) + log Transfer(msg.sender, ZERO_ADDRESS, _value) + + +@view +@external +def allowance(_owner : address, _spender : address) -> uint256: + """ + @notice Check the amount of tokens that an owner allowed to a spender + @param _owner The address which owns the funds + @param _spender The address which will spend the funds + @return uint256 Amount of tokens still available for the spender + """ + return self.allowances[_owner][_spender] + + +@internal +def _transfer(_from: address, _to: address, _value: uint256): + self._checkpoint(_from) + self._checkpoint(_to) + reward_contract: address = self.reward_contract + + if _value != 0: + total_supply: uint256 = self.totalSupply + if reward_contract != ZERO_ADDRESS: + self._checkpoint_rewards(_from, total_supply) + new_balance: uint256 = self.balanceOf[_from] - _value + self.balanceOf[_from] = new_balance + self._update_liquidity_limit(_from, new_balance, total_supply) + + if reward_contract != ZERO_ADDRESS: + self._checkpoint_rewards(_to, total_supply) + new_balance = self.balanceOf[_to] + _value + self.balanceOf[_to] = new_balance + self._update_liquidity_limit(_to, new_balance, total_supply) + + log Transfer(_from, _to, _value) + + +@external +@nonreentrant('lock') +def transfer(_to : address, _value : uint256) -> bool: + """ + @notice Transfer token for a specified address + @dev Transferring claims pending reward tokens for the sender and receiver + @param _to The address to transfer to. + @param _value The amount to be transferred. + """ + self._transfer(msg.sender, _to, _value) + + return True + + +@external +@nonreentrant('lock') +def transferFrom(_from : address, _to : address, _value : uint256) -> bool: + """ + @notice Transfer tokens from one address to another. + @dev Transferring claims pending reward tokens for the sender and receiver + @param _from address The address which you want to send tokens from + @param _to address The address which you want to transfer to + @param _value uint256 the amount of tokens to be transferred + """ + _allowance: uint256 = self.allowances[_from][msg.sender] + if _allowance != MAX_UINT256: + self.allowances[_from][msg.sender] = _allowance - _value + + self._transfer(_from, _to, _value) + + return True + + +@external +def approve(_spender : address, _value : uint256) -> bool: + """ + @notice Approve the passed address to transfer the specified amount of + tokens on behalf of msg.sender + @dev Beware that changing an allowance via this method brings the risk + that someone may use both the old and new allowance by unfortunate + transaction ordering. This may be mitigated with the use of + {incraseAllowance} and {decreaseAllowance}. + https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + @param _spender The address which will transfer the funds + @param _value The amount of tokens that may be transferred + @return bool success + """ + self.allowances[msg.sender][_spender] = _value + log Approval(msg.sender, _spender, _value) + + return True + + +@external +def increaseAllowance(_spender: address, _added_value: uint256) -> bool: + """ + @notice Increase the allowance granted to `_spender` by the caller + @dev This is alternative to {approve} that can be used as a mitigation for + the potential race condition + @param _spender The address which will transfer the funds + @param _added_value The amount of to increase the allowance + @return bool success + """ + allowance: uint256 = self.allowances[msg.sender][_spender] + _added_value + self.allowances[msg.sender][_spender] = allowance + + log Approval(msg.sender, _spender, allowance) + + return True + + +@external +def decreaseAllowance(_spender: address, _subtracted_value: uint256) -> bool: + """ + @notice Decrease the allowance granted to `_spender` by the caller + @dev This is alternative to {approve} that can be used as a mitigation for + the potential race condition + @param _spender The address which will transfer the funds + @param _subtracted_value The amount of to decrease the allowance + @return bool success + """ + allowance: uint256 = self.allowances[msg.sender][_spender] - _subtracted_value + self.allowances[msg.sender][_spender] = allowance + + log Approval(msg.sender, _spender, allowance) + + return True + + +@external +@nonreentrant('lock') +def set_rewards(_reward_contract: address, _sigs: bytes32, _reward_tokens: address[MAX_REWARDS]): + """ + @notice Set the active reward contract + @dev A reward contract cannot be set while this contract has no deposits + @param _reward_contract Reward contract address. Set to ZERO_ADDRESS to + disable staking. + @param _sigs Four byte selectors for staking, withdrawing and claiming, + right padded with zero bytes. If the reward contract can + be claimed from but does not require staking, the staking + and withdraw selectors should be set to 0x00 + @param _reward_tokens List of claimable tokens for this reward contract + """ + assert msg.sender == self.admin + + lp_token: address = self.lp_token + current_reward_contract: address = self.reward_contract + total_supply: uint256 = self.totalSupply + if current_reward_contract != ZERO_ADDRESS: + self._checkpoint_rewards(ZERO_ADDRESS, total_supply) + withdraw_sig: Bytes[4] = slice(self.reward_sigs, 4, 4) + if convert(withdraw_sig, uint256) != 0: + if total_supply != 0: + raw_call( + current_reward_contract, + concat(withdraw_sig, convert(total_supply, bytes32)) + ) + ERC20(lp_token).approve(current_reward_contract, 0) + + if _reward_contract != ZERO_ADDRESS: + assert _reward_contract.is_contract # dev: not a contract + sigs: bytes32 = _sigs + deposit_sig: Bytes[4] = slice(sigs, 0, 4) + withdraw_sig: Bytes[4] = slice(sigs, 4, 4) + + if convert(deposit_sig, uint256) != 0: + # need a non-zero total supply to verify the sigs + assert total_supply != 0 # dev: zero total supply + ERC20(lp_token).approve(_reward_contract, MAX_UINT256) + + # it would be Very Bad if we get the signatures wrong here, so + # we do a test deposit and withdrawal prior to setting them + raw_call( + _reward_contract, + concat(deposit_sig, convert(total_supply, bytes32)) + ) # dev: failed deposit + assert ERC20(lp_token).balanceOf(self) == 0 + raw_call( + _reward_contract, + concat(withdraw_sig, convert(total_supply, bytes32)) + ) # dev: failed withdraw + assert ERC20(lp_token).balanceOf(self) == total_supply + + # deposit and withdraw are good, time to make the actual deposit + raw_call( + _reward_contract, + concat(deposit_sig, convert(total_supply, bytes32)) + ) + else: + assert convert(withdraw_sig, uint256) == 0 # dev: withdraw without deposit + + self.reward_contract = _reward_contract + self.reward_sigs = _sigs + for i in range(MAX_REWARDS): + if _reward_tokens[i] != ZERO_ADDRESS: + self.reward_tokens[i] = _reward_tokens[i] + elif self.reward_tokens[i] != ZERO_ADDRESS: + self.reward_tokens[i] = ZERO_ADDRESS + else: + assert i != 0 # dev: no reward token + break + + if _reward_contract != ZERO_ADDRESS: + # do an initial checkpoint to verify that claims are working + self._checkpoint_rewards(ZERO_ADDRESS, total_supply) + + +@external +def set_killed(_is_killed: bool): + """ + @notice Set the killed status for this contract + @dev When killed, the gauge always yields a rate of 0 and so cannot mint CRV + @param _is_killed Killed status to set + """ + assert msg.sender == self.admin + + self.is_killed = _is_killed + + +@external +def commit_transfer_ownership(addr: address): + """ + @notice Transfer ownership of GaugeController to `addr` + @param addr Address to have ownership transferred to + """ + assert msg.sender == self.admin # dev: admin only + + self.future_admin = addr + log CommitOwnership(addr) + + +@external +def accept_transfer_ownership(): + """ + @notice Accept a pending ownership transfer + """ + _admin: address = self.future_admin + assert msg.sender == _admin # dev: future admin only + + self.admin = _admin + log ApplyOwnership(_admin) diff --git a/contracts/pools/ETHX/Pooldata.json b/contracts/pools/ETHX/Pooldata.json new file mode 100644 index 00000000..9283bc2e --- /dev/null +++ b/contracts/pools/ETHX/Pooldata.json @@ -0,0 +1,38 @@ +{ + "lp_contract": "CurveTokenV2", + "swap_address": "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7", + "lp_token_address": "0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490", + "gauge_addresses": ["0xbFcF63294aD7105dEa65aA58F8AE5BE2D9d0952A"], + "lp_constructor": { + "name": "Curve.fi DAI/USDC/USDT/ETHX", + "symbol": "4Crv" + }, + "coins": [ + { + "name": "DAI", + "decimals": 18, + "tethered": false, + "underlying_address": "0x6b175474e89094c44da98b954eedeac495271d0f" + }, + { + "name": "USDC", + "decimals": 6, + "tethered": false, + "underlying_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" + + }, + { + "name": "ETHX", + "decimals": 18, + "tethered": false, + "underlying_address": "0x8487B97c91ecC1a03b4907B64Bdeab306B888c0E" + + }, + { + "name": "USDT", + "decimals": 6, + "tethered": true, + "underlying_address": "0xdac17f958d2ee523a2206206994597c13d831ec7" + } + ] +} diff --git a/contracts/pools/ETHX/StableSwapETHX.vy b/contracts/pools/ETHX/StableSwapETHX.vy new file mode 100644 index 00000000..7db02f8f --- /dev/null +++ b/contracts/pools/ETHX/StableSwapETHX.vy @@ -0,0 +1,843 @@ + +# @version 0.2.8 +""" +@title ETH/ETHX StableSwap +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020 - all rights reserved +""" + +from vyper.interfaces import ERC20 + +# External Contracts +interface aETH: + def ratio() -> uint256: view + + +interface CurveToken: + def mint(_to: address, _value: uint256) -> bool: nonpayable + def burnFrom(_to: address, _value: uint256) -> bool: nonpayable + + +# Events +event TokenExchange: + buyer: indexed(address) + sold_id: int128 + tokens_sold: uint256 + bought_id: int128 + tokens_bought: uint256 + +event AddLiquidity: + provider: indexed(address) + token_amounts: uint256[N_COINS] + fees: uint256[N_COINS] + invariant: uint256 + token_supply: uint256 + +event RemoveLiquidity: + provider: indexed(address) + token_amounts: uint256[N_COINS] + fees: uint256[N_COINS] + token_supply: uint256 + +event RemoveLiquidityOne: + provider: indexed(address) + token_amount: uint256 + coin_amount: uint256 + +event RemoveLiquidityImbalance: + provider: indexed(address) + token_amounts: uint256[N_COINS] + fees: uint256[N_COINS] + invariant: uint256 + token_supply: uint256 + +event CommitNewAdmin: + deadline: indexed(uint256) + admin: indexed(address) + +event NewAdmin: + admin: indexed(address) + +event CommitNewFee: + deadline: indexed(uint256) + fee: uint256 + admin_fee: uint256 + +event NewFee: + fee: uint256 + admin_fee: uint256 + +event RampA: + old_A: uint256 + new_A: uint256 + initial_time: uint256 + future_time: uint256 + +event StopRampA: + A: uint256 + t: uint256 + + +# These constants must be set prior to compiling +N_COINS: constant(int128) = 2 + +# fixed constants +FEE_DENOMINATOR: constant(uint256) = 10 ** 10 +LENDING_PRECISION: constant(uint256) = 10 ** 18 +PRECISION: constant(uint256) = 10 ** 18 # The precision to convert to + +MAX_ADMIN_FEE: constant(uint256) = 10 * 10 ** 9 +MAX_FEE: constant(uint256) = 5 * 10 ** 9 +MAX_A: constant(uint256) = 10 ** 6 +MAX_A_CHANGE: constant(uint256) = 10 + +ADMIN_ACTIONS_DELAY: constant(uint256) = 3 * 86400 +MIN_RAMP_TIME: constant(uint256) = 86400 + +coins: public(address[N_COINS]) +balances: public(uint256[N_COINS]) + +fee: public(uint256) # fee * 1e10 +admin_fee: public(uint256) # admin_fee * 1e10 + +owner: public(address) +lp_token: public(address) + +A_PRECISION: constant(uint256) = 100 +initial_A: public(uint256) +future_A: public(uint256) +initial_A_time: public(uint256) +future_A_time: public(uint256) + +admin_actions_deadline: public(uint256) +transfer_ownership_deadline: public(uint256) +future_fee: public(uint256) +future_admin_fee: public(uint256) +future_owner: public(address) + +is_killed: bool +kill_deadline: uint256 +KILL_DEADLINE_DT: constant(uint256) = 2 * 30 * 86400 + + +@external +def __init__( + _owner: address, + _coins: address[N_COINS], + _pool_token: address, + _A: uint256, + _fee: uint256, + _admin_fee: uint256, +): + """ + @notice Contract constructor + @param _owner Contract owner address + @param _coins Addresses of ERC20 contracts of wrapped coins + @param _pool_token Address of the token representing LP share + @param _A Amplification coefficient multiplied by n * (n - 1) + @param _fee Fee to charge for exchanges + @param _admin_fee Admin fee + """ + + assert _coins[0] == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE + assert _coins[1] != ZERO_ADDRESS + + self.coins = _coins + self.initial_A = _A * A_PRECISION + self.future_A = _A * A_PRECISION + self.fee = _fee + self.admin_fee = _admin_fee + self.owner = _owner + self.kill_deadline = block.timestamp + KILL_DEADLINE_DT + self.lp_token = _pool_token + + +@view +@internal +def _A() -> uint256: + """ + Handle ramping A up or down + """ + t1: uint256 = self.future_A_time + A1: uint256 = self.future_A + + if block.timestamp < t1: + A0: uint256 = self.initial_A + t0: uint256 = self.initial_A_time + # Expressions in uint256 cannot have negative numbers, thus "if" + if A1 > A0: + return A0 + (A1 - A0) * (block.timestamp - t0) / (t1 - t0) + else: + return A0 - (A0 - A1) * (block.timestamp - t0) / (t1 - t0) + + else: # when t1 == 0 or block.timestamp >= t1 + return A1 + + +@view +@external +def A() -> uint256: + return self._A() / A_PRECISION + + +@view +@external +def A_precise() -> uint256: + return self._A() + + +@view +@internal +def _stored_rates() -> uint256[N_COINS]: + return [ + convert(PRECISION, uint256), + PRECISION * LENDING_PRECISION / aETH(self.coins[1]).ratio() + ] + + +@view +@internal +def _xp(rates: uint256[N_COINS]) -> uint256[N_COINS]: + result: uint256[N_COINS] = rates + for i in range(N_COINS): + result[i] = result[i] * self.balances[i] / PRECISION + return result + + +@internal +@view +def get_D(xp: uint256[N_COINS], amp: uint256) -> uint256: + S: uint256 = 0 + Dprev: uint256 = 0 + + for _x in xp: + S += _x + if S == 0: + return 0 + + D: uint256 = S + Ann: uint256 = amp * N_COINS + for _i in range(255): + D_P: uint256 = D + for _x in xp: + D_P = D_P * D / (_x * N_COINS) # If division by 0, this will be borked: only withdrawal will work. And that is good + Dprev = D + D = (Ann * S / A_PRECISION + D_P * N_COINS) * D / ((Ann - A_PRECISION) * D / A_PRECISION + (N_COINS + 1) * D_P) + # Equality with the precision of 1 + if D > Dprev: + if D - Dprev <= 1: + return D + else: + if Dprev - D <= 1: + return D + # convergence typically occurs in 4 rounds or less, this should be unreachable! + # if it does happen the pool is borked and LPs can withdraw via `remove_liquidity` + raise + + +@view +@internal +def get_D_mem(rates: uint256[N_COINS], _balances: uint256[N_COINS], amp: uint256) -> uint256: + result: uint256[N_COINS] = rates + for i in range(N_COINS): + result[i] = result[i] * _balances[i] / PRECISION + return self.get_D(result, amp) + + +@view +@external +def get_virtual_price() -> uint256: + """ + @notice The current virtual price of the pool LP token + @dev Useful for calculating profits + @return LP token virtual price normalized to 1e18 + """ + D: uint256 = self.get_D(self._xp(self._stored_rates()), self._A()) + # D is in the units similar to DAI (e.g. converted to precision 1e18) + # When balanced, D = n * x_u - total virtual value of the portfolio + token_supply: uint256 = ERC20(self.lp_token).totalSupply() + return D * PRECISION / token_supply + + +@view +@external +def calc_token_amount(amounts: uint256[N_COINS], is_deposit: bool) -> uint256: + """ + @notice Calculate addition or reduction in token supply from a deposit or withdrawal + @dev This calculation accounts for slippage, but not fees. + Needed to prevent front-running, not for precise calculations! + @param amounts Amount of each coin being deposited + @param is_deposit set True for deposits, False for withdrawals + @return Expected amount of LP tokens received + """ + amp: uint256 = self._A() + rates: uint256[N_COINS] = self._stored_rates() + _balances: uint256[N_COINS] = self.balances + D0: uint256 = self.get_D_mem(rates, _balances, amp) + for i in range(N_COINS): + _amount: uint256 = amounts[i] + if is_deposit: + _balances[i] += _amount + else: + _balances[i] -= _amount + D1: uint256 = self.get_D_mem(rates, _balances, amp) + token_amount: uint256 = ERC20(self.lp_token).totalSupply() + diff: uint256 = 0 + if is_deposit: + diff = D1 - D0 + else: + diff = D0 - D1 + return diff * token_amount / D0 + +@payable +@external +@nonreentrant('lock') +def add_liquidity(amounts: uint256[N_COINS], min_mint_amount: uint256) -> uint256: + """ + @notice Deposit coins into the pool + @param amounts List of amounts of coins to deposit + @param min_mint_amount Minimum amount of LP tokens to mint from the deposit + @return Amount of LP tokens received by depositing + """ + assert not self.is_killed + amp: uint256 = self._A() + rates: uint256[N_COINS] = self._stored_rates() + _lp_token: address = self.lp_token + token_supply: uint256 = ERC20(_lp_token).totalSupply() + + # Initial invariant + D0: uint256 = 0 + old_balances: uint256[N_COINS] = self.balances + if token_supply != 0: + D0 = self.get_D_mem(rates, old_balances, amp) + + new_balances: uint256[N_COINS] = old_balances + for i in range(N_COINS): + if token_supply == 0: + assert amounts[i] > 0 + new_balances[i] += amounts[i] + + # Invariant after change + D1: uint256 = self.get_D_mem(rates, new_balances, amp) + assert D1 > D0 + + # We need to recalculate the invariant accounting for fees + # to calculate fair user's share + D2: uint256 = D1 + fees: uint256[N_COINS] = empty(uint256[N_COINS]) + mint_amount: uint256 = 0 + if token_supply != 0: + # Only account for fees if we are not the first to deposit + _fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + _admin_fee: uint256 = self.admin_fee + for i in range(N_COINS): + ideal_balance: uint256 = D1 * old_balances[i] / D0 + difference: uint256 = 0 + if ideal_balance > new_balances[i]: + difference = ideal_balance - new_balances[i] + else: + difference = new_balances[i] - ideal_balance + fees[i] = _fee * difference / FEE_DENOMINATOR + self.balances[i] = new_balances[i] - (fees[i] * _admin_fee / FEE_DENOMINATOR) + new_balances[i] -= fees[i] + D2 = self.get_D_mem(rates, new_balances, amp) + mint_amount = token_supply * (D2 - D0) / D0 + else: + self.balances = new_balances + mint_amount = D1 # Take the dust if there was any + + assert mint_amount >= min_mint_amount, "Slippage screwed you" + + # Take coins from the sender + assert msg.value == amounts[0] + if amounts[1] > 0: + assert ERC20(self.coins[1]).transferFrom(msg.sender, self, amounts[1]) + + # Mint pool tokens + CurveToken(_lp_token).mint(msg.sender, mint_amount) + + log AddLiquidity(msg.sender, amounts, fees, D1, token_supply + mint_amount) + + return mint_amount + + +@view +@internal +def get_y(i: int128, j: int128, x: uint256, xp_: uint256[N_COINS]) -> uint256: + # x in the input is converted to the same price/precision + + assert i != j # dev: same coin + assert j >= 0 # dev: j below zero + assert j < N_COINS # dev: j above N_COINS + + # should be unreachable, but good for safety + assert i >= 0 + assert i < N_COINS + + A_: uint256 = self._A() + D: uint256 = self.get_D(xp_, A_) + Ann: uint256 = A_ * N_COINS + c: uint256 = D + S_: uint256 = 0 + _x: uint256 = 0 + y_prev: uint256 = 0 + + for _i in range(N_COINS): + if _i == i: + _x = x + elif _i != j: + _x = xp_[_i] + else: + continue + S_ += _x + c = c * D / (_x * N_COINS) + c = c * D * A_PRECISION / (Ann * N_COINS) + b: uint256 = S_ + D * A_PRECISION / Ann # - D + y: uint256 = D + for _i in range(255): + y_prev = y + y = (y*y + c) / (2 * y + b - D) + # Equality with the precision of 1 + if y > y_prev: + if y - y_prev <= 1: + return y + else: + if y_prev - y <= 1: + return y + raise + + +@view +@external +def get_dy(i: int128, j: int128, dx: uint256) -> uint256: + # dx and dy in c-units + rates: uint256[N_COINS] = self._stored_rates() + xp: uint256[N_COINS] = self._xp(rates) + + x: uint256 = xp[i] + dx * rates[i] / PRECISION + y: uint256 = self.get_y(i, j, x, xp) + dy: uint256 = xp[j] - y + _fee: uint256 = self.fee * dy / FEE_DENOMINATOR + return (dy - _fee) * PRECISION / rates[j] + + +@view +@external +def get_dx(i: int128, j: int128, dy: uint256) -> uint256: + # dx and dy in c-units + rates: uint256[N_COINS] = self._stored_rates() + xp: uint256[N_COINS] = self._xp(rates) + + y: uint256 = xp[j] - (dy * FEE_DENOMINATOR / (FEE_DENOMINATOR - self.fee)) * rates[j] / PRECISION + x: uint256 = self.get_y(j, i, y, xp) + dx: uint256 = (x - xp[i]) * PRECISION / rates[i] + return dx + + +@payable +@external +@nonreentrant('lock') +def exchange(i: int128, j: int128, dx: uint256, min_dy: uint256) -> uint256: + """ + @notice Perform an exchange between two coins + @dev Index values can be found via the `coins` public getter method + @param i Index value for the coin to send + @param j Index valie of the coin to recieve + @param dx Amount of `i` being exchanged + @param min_dy Minimum amount of `j` to receive + @return Actual amount of `j` received + """ + assert not self.is_killed # dev: is killed + + rates: uint256[N_COINS] = self._stored_rates() + + xp: uint256[N_COINS] = self._xp(rates) + x: uint256 = xp[i] + dx * rates[i] / PRECISION + y: uint256 = self.get_y(i, j, x, xp) + dy: uint256 = xp[j] - y - 1 # -1 just in case there were some rounding errors + dy_fee: uint256 = dy * self.fee / FEE_DENOMINATOR + dy_admin_fee: uint256 = dy_fee * self.admin_fee / FEE_DENOMINATOR + + self.balances[i] = x * PRECISION / rates[i] + self.balances[j] = (y + (dy_fee - dy_admin_fee)) * PRECISION / rates[j] + + dy = (dy - dy_fee) * PRECISION / rates[j] + assert dy >= min_dy, "Exchange resulted in fewer coins than expected" + + coin: address = self.coins[1] + if i == 0: + assert msg.value == dx + assert ERC20(coin).transfer(msg.sender, dy) + else: + assert msg.value == 0 + assert ERC20(coin).transferFrom(msg.sender, self, dx) + raw_call(msg.sender, b"", value=dy) + + log TokenExchange(msg.sender, i, dx, j, dy) + + return dy + + +@external +@nonreentrant('lock') +def remove_liquidity(_amount: uint256, min_amounts: uint256[N_COINS]) -> uint256[N_COINS]: + """ + @notice Withdraw coins from the pool + @dev Withdrawal amounts are based on current deposit ratios + @param _amount Quantity of LP tokens to burn in the withdrawal + @param min_amounts Minimum amounts of underlying coins to receive + @return List of amounts of coins that were withdrawn + """ + _lp_token: address = self.lp_token + total_supply: uint256 = ERC20(_lp_token).totalSupply() + amounts: uint256[N_COINS] = empty(uint256[N_COINS]) + + for i in range(N_COINS): + _balance: uint256 = self.balances[i] + value: uint256 = _balance * _amount / total_supply + assert value >= min_amounts[i], "Withdrawal resulted in fewer coins than expected" + self.balances[i] = _balance - value + amounts[i] = value + if i == 0: + raw_call(msg.sender, b"", value=value) + else: + assert ERC20(self.coins[1]).transfer(msg.sender, value) + + CurveToken(_lp_token).burnFrom(msg.sender, _amount) # Will raise if not enough + + log RemoveLiquidity(msg.sender, amounts, empty(uint256[N_COINS]), total_supply - _amount) + + return amounts + + +@external +@nonreentrant('lock') +def remove_liquidity_imbalance(amounts: uint256[N_COINS], max_burn_amount: uint256) -> uint256: + """ + @notice Withdraw coins from the pool in an imbalanced amount + @param amounts List of amounts of underlying coins to withdraw + @param max_burn_amount Maximum amount of LP token to burn in the withdrawal + @return Actual amount of the LP token burned in the withdrawal + """ + assert not self.is_killed + amp: uint256 = self._A() + rates: uint256[N_COINS] = self._stored_rates() + old_balances: uint256[N_COINS] = self.balances + D0: uint256 = self.get_D_mem(rates, old_balances, amp) + + new_balances: uint256[N_COINS] = old_balances + for i in range(N_COINS): + new_balances[i] -= amounts[i] + D1: uint256 = self.get_D_mem(rates, new_balances, amp) + + fees: uint256[N_COINS] = empty(uint256[N_COINS]) + _fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + _admin_fee: uint256 = self.admin_fee + for i in range(N_COINS): + ideal_balance: uint256 = D1 * old_balances[i] / D0 + new_balance: uint256 = new_balances[i] + difference: uint256 = 0 + if ideal_balance > new_balance: + difference = ideal_balance - new_balance + else: + difference = new_balance - ideal_balance + fees[i] = _fee * difference / FEE_DENOMINATOR + self.balances[i] = new_balance - (fees[i] * _admin_fee / FEE_DENOMINATOR) + new_balances[i] = new_balance - fees[i] + D2: uint256 = self.get_D_mem(rates, new_balances, amp) + + lp_token: address = self.lp_token + token_supply: uint256 = ERC20(lp_token).totalSupply() + token_amount: uint256 = (D0 - D2) * token_supply / D0 + assert token_amount != 0 + assert token_amount <= max_burn_amount, "Slippage screwed you" + + CurveToken(lp_token).burnFrom(msg.sender, token_amount) # dev: insufficient funds + + if amounts[0] != 0: + raw_call(msg.sender, b"", value=amounts[0]) + if amounts[1] != 0: + assert ERC20(self.coins[1]).transfer(msg.sender, amounts[1]) + + log RemoveLiquidityImbalance(msg.sender, amounts, fees, D1, token_supply - token_amount) + + return token_amount + + +@pure +@internal +def get_y_D(A_: uint256, i: int128, xp: uint256[N_COINS], D: uint256) -> uint256: + """ + Calculate x[i] if one reduces D from being calculated for xp to D + + Done by solving quadratic equation iteratively. + x_1**2 + x_1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) + x_1**2 + b*x_1 = c + + x_1 = (x_1**2 + c) / (2*x_1 + b) + """ + # x in the input is converted to the same price/precision + + assert i >= 0 # dev: i below zero + assert i < N_COINS # dev: i above N_COINS + + Ann: uint256 = A_ * N_COINS + c: uint256 = D + S_: uint256 = 0 + _x: uint256 = 0 + y_prev: uint256 = 0 + + for _i in range(N_COINS): + if _i != i: + _x = xp[_i] + else: + continue + S_ += _x + c = c * D / (_x * N_COINS) + c = c * D * A_PRECISION / (Ann * N_COINS) + b: uint256 = S_ + D * A_PRECISION / Ann + y: uint256 = D + + for _i in range(255): + y_prev = y + y = (y*y + c) / (2 * y + b - D) + # Equality with the precision of 1 + if y > y_prev: + if y - y_prev <= 1: + return y + else: + if y_prev - y <= 1: + return y + raise + + +@view +@internal +def _calc_withdraw_one_coin(_token_amount: uint256, i: int128) -> (uint256, uint256): + # First, need to calculate + # * Get current D + # * Solve Eqn against y_i for D - _token_amount + amp: uint256 = self._A() + rates: uint256[N_COINS] = self._stored_rates() + xp: uint256[N_COINS] = self._xp(rates) + D0: uint256 = self.get_D(xp, amp) + + total_supply: uint256 = ERC20(self.lp_token).totalSupply() + + D1: uint256 = D0 - _token_amount * D0 / total_supply + new_y: uint256 = self.get_y_D(amp, i, xp, D1) + + xp_reduced: uint256[N_COINS] = xp + _fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + + for j in range(N_COINS): + dx_expected: uint256 = 0 + xp_j: uint256 = xp[j] + if j == i: + dx_expected = xp_j * D1 / D0 - new_y + else: + dx_expected = xp_j - xp_j * D1 / D0 + xp_reduced[j] -= _fee * dx_expected / FEE_DENOMINATOR + + dy: uint256 = xp_reduced[i] - self.get_y_D(amp, i, xp_reduced, D1) + rate: uint256 = rates[i] + dy = (dy - 1) * PRECISION / rate # Withdraw less to account for rounding errors + dy_0: uint256 = (xp[i] - new_y) * PRECISION / rate # w/o fees + + return dy, dy_0 - dy + + +@view +@external +def calc_withdraw_one_coin(_token_amount: uint256, i: int128) -> uint256: + """ + @notice Calculate the amount received when withdrawing a single coin + @param _token_amount Amount of LP tokens to burn in the withdrawal + @param i Index value of the coin to withdraw + @return Amount of coin received + """ + return self._calc_withdraw_one_coin(_token_amount, i)[0] + + +@external +@nonreentrant('lock') +def remove_liquidity_one_coin(_token_amount: uint256, i: int128, _min_amount: uint256) -> uint256: + """ + @notice Withdraw a single coin from the pool + @param _token_amount Amount of LP tokens to burn in the withdrawal + @param i Index value of the coin to withdraw + @param _min_amount Minimum amount of coin to receive + @return Amount of coin received + """ + assert not self.is_killed # dev: is killed + + dy: uint256 = 0 + dy_fee: uint256 = 0 + dy, dy_fee = self._calc_withdraw_one_coin(_token_amount, i) + assert dy >= _min_amount, "Not enough coins removed" + + self.balances[i] -= (dy + dy_fee * self.admin_fee / FEE_DENOMINATOR) + CurveToken(self.lp_token).burnFrom(msg.sender, _token_amount) # dev: insufficient funds + + if i == 0: + raw_call(msg.sender, b"", value=dy) + else: + assert ERC20(self.coins[1]).transfer(msg.sender, dy) + + log RemoveLiquidityOne(msg.sender, _token_amount, dy) + + return dy + + +### Admin functions ### +@external +def ramp_A(_future_A: uint256, _future_time: uint256): + assert msg.sender == self.owner # dev: only owner + assert block.timestamp >= self.initial_A_time + MIN_RAMP_TIME + assert _future_time >= block.timestamp + MIN_RAMP_TIME # dev: insufficient time + + _initial_A: uint256 = self._A() + _future_A_p: uint256 = _future_A * A_PRECISION + + assert _future_A > 0 and _future_A < MAX_A + if _future_A_p < _initial_A: + assert _future_A_p * MAX_A_CHANGE >= _initial_A + else: + assert _future_A_p <= _initial_A * MAX_A_CHANGE + + self.initial_A = _initial_A + self.future_A = _future_A_p + self.initial_A_time = block.timestamp + self.future_A_time = _future_time + + log RampA(_initial_A, _future_A_p, block.timestamp, _future_time) + + +@external +def stop_ramp_A(): + assert msg.sender == self.owner # dev: only owner + + current_A: uint256 = self._A() + self.initial_A = current_A + self.future_A = current_A + self.initial_A_time = block.timestamp + self.future_A_time = block.timestamp + # now (block.timestamp < t1) is always False, so we return saved A + + log StopRampA(current_A, block.timestamp) + + +@external +def commit_new_fee(new_fee: uint256, new_admin_fee: uint256): + assert msg.sender == self.owner # dev: only owner + assert self.admin_actions_deadline == 0 # dev: active action + assert new_fee <= MAX_FEE # dev: fee exceeds maximum + assert new_admin_fee <= MAX_ADMIN_FEE # dev: admin fee exceeds maximum + + _deadline: uint256 = block.timestamp + ADMIN_ACTIONS_DELAY + self.admin_actions_deadline = _deadline + self.future_fee = new_fee + self.future_admin_fee = new_admin_fee + + log CommitNewFee(_deadline, new_fee, new_admin_fee) + + +@external +@nonreentrant('lock') +def apply_new_fee(): + assert msg.sender == self.owner # dev: only owner + assert block.timestamp >= self.admin_actions_deadline # dev: insufficient time + assert self.admin_actions_deadline != 0 # dev: no active action + + self.admin_actions_deadline = 0 + _fee: uint256 = self.future_fee + _admin_fee: uint256 = self.future_admin_fee + self.fee = _fee + self.admin_fee = _admin_fee + + log NewFee(_fee, _admin_fee) + + +@external +def revert_new_parameters(): + assert msg.sender == self.owner # dev: only owner + + self.admin_actions_deadline = 0 + + +@external +def commit_transfer_ownership(_owner: address): + assert msg.sender == self.owner # dev: only owner + assert self.transfer_ownership_deadline == 0 # dev: active transfer + + _deadline: uint256 = block.timestamp + ADMIN_ACTIONS_DELAY + self.transfer_ownership_deadline = _deadline + self.future_owner = _owner + + log CommitNewAdmin(_deadline, _owner) + + +@external +@nonreentrant('lock') +def apply_transfer_ownership(): + assert msg.sender == self.owner # dev: only owner + assert block.timestamp >= self.transfer_ownership_deadline # dev: insufficient time + assert self.transfer_ownership_deadline != 0 # dev: no active transfer + + self.transfer_ownership_deadline = 0 + _owner: address = self.future_owner + self.owner = _owner + + log NewAdmin(_owner) + + +@external +def revert_transfer_ownership(): + assert msg.sender == self.owner # dev: only owner + self.transfer_ownership_deadline = 0 + + +@view +@external +def admin_balances(i: uint256) -> uint256: + if i == 0: + return self.balance - self.balances[0] + return ERC20(self.coins[i]).balanceOf(self) - self.balances[i] + + +@external +@nonreentrant('lock') +def withdraw_admin_fees(): + assert msg.sender == self.owner # dev: only owner + + amount: uint256 = self.balance - self.balances[0] + if amount != 0: + raw_call(msg.sender, b"", value=amount) + + amount = ERC20(self.coins[1]).balanceOf(self) - self.balances[1] + if amount != 0: + assert ERC20(self.coins[1]).transfer(msg.sender, amount) + + +@external +@nonreentrant('lock') +def donate_admin_fees(): + assert msg.sender == self.owner # dev: only owner + for i in range(N_COINS): + if i == 0: + self.balances[0] = self.balance + else: + self.balances[i] = ERC20(self.coins[i]).balanceOf(self) + + +@external +def kill_me(): + assert msg.sender == self.owner # dev: only owner + assert self.kill_deadline > block.timestamp # dev: deadline has passed + self.is_killed = True + + +@external +def unkill_me(): + assert msg.sender == self.owner # dev: only owner + self.is_killed = False diff --git a/contracts/pools/ETHX/ratecalculatorETHX.vy b/contracts/pools/ETHX/ratecalculatorETHX.vy new file mode 100644 index 00000000..765cb0d1 --- /dev/null +++ b/contracts/pools/ETHX/ratecalculatorETHX.vy @@ -0,0 +1,22 @@ +# @version 0.2.11 +""" +@title Curve ETHX Pool Rate Calculator +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2021 - all rights reserved +@notice Logic for calculating exchange rate between ETHX -> ETH +""" + +interface ETHX: + def ratio() -> uint256: view + + +@view +@external +def get_rate(_coin: address) -> uint256: + """ + @notice Calculate the exchange rate for 1 ETHX -> ETH + @param _coin The ETHX contract address + @return The exchange rate of 1 ETHX in ETH + """ + result: uint256 = ETHX(_coin).ratio() + return 10 ** 36 / result diff --git a/contracts/pools/ETHX/readme.md b/contracts/pools/ETHX/readme.md new file mode 100644 index 00000000..569665b3 --- /dev/null +++ b/contracts/pools/ETHX/readme.md @@ -0,0 +1,20 @@ +# curve-contract/contracts/pools/ethx + +[Curve ETHX]() + +## Contracts + +- [`StableSwapETHX`](StableSwapETHX.vy): Curve stablecoin AMM contract + +## Deployments + +- [`CurveContractV3`](../../tokens/CurveTokenV3.vy): +- [`LiquidityGaugeV2`](https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/gauges/LiquidityGaugeV2.vy): +- [`StableSwapETHX`](StableSwapETHX.vy): + +## Stablecoins + +Curve ETHX pool supports swaps between ETH and [`VersoriumX`](https://github.com/VersoriumX) staked ETH (ETHX): + +- `ETH`: represented in the pool as `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` +- `ETHX`: [0x8487B97c91ecC1a03b4907B64Bdeab306B888c0E](https://etherscan.io/address/0x8487B97c91ecC1a03b4907B64Bdeab306B888c0E#code) diff --git a/contracts/pools/y/StableSwapY.vy b/contracts/pools/y/StableSwapY.vy index 2ffc9a44..61e2bd01 100644 --- a/contracts/pools/y/StableSwapY.vy +++ b/contracts/pools/y/StableSwapY.vy @@ -21,7 +21,7 @@ contract ERC20m: # External Contracts -contract yERC20: +contract XERC20: def totalSupply() -> uint256: constant def allowance(_owner: address, _spender: address) -> uint256: constant def transfer(_to: address, _value: uint256) -> bool: modifying diff --git a/contracts/pools/y/pooldata.json b/contracts/pools/y/pooldata.json index db2a3977..de1000f4 100644 --- a/contracts/pools/y/pooldata.json +++ b/contracts/pools/y/pooldata.json @@ -1,17 +1,17 @@ { "lp_contract": "CurveTokenV1", - "wrapped_contract": "yERC20", + "wrapped_contract": "XERC20", "swap_address": "0x45F783CCE6B7FF23B2ab2D70e416cdb7D6055f51", - "lp_token_address": "0xdF5e0e81Dff6FAF3A7e52BA697820c5e32D806A8", + "lp_token_address": "0x8487B97c91ecC1a03b4907B64Bdeab306B888c0E", "zap_address": "0xbbc81d23ea2c3ec7e56d39296f0cbb648873a5d3", "gauge_addresses": ["0xFA712EE4788C042e2B7BB55E6cb8ec569C4530c1"], "lp_constructor": { - "name": "Curve.fi yDAI/yUSDC/yUSDT/yTUSD", - "symbol": "yDAI+yUSDC+yUSDT+yTUSD" + "name": "Curve.fi XDAI/XUSDC/XUSDT/XTUSD", + "symbol": "XDAI+XUSDC+XUSDT+XTUSD" }, "coins": [ { - "name": "yDAI", + "name": "XDAI", "underlying_name": "DAI", "decimals": 18, "tethered": false, @@ -20,7 +20,7 @@ "wrapped_address": "0x16de59092dAE5CcF4A1E6439D611fd0653f0Bd01" }, { - "name": "yUSDC", + "name": "XUSDC", "underlying_name": "USDC", "decimals": 6, "tethered": false, @@ -29,7 +29,7 @@ "wrapped_address": "0xd6aD7a6750A7593E092a9B218d66C0A814a3436e" }, { - "name": "yUSDT", + "name": "XUSDT", "underlying_name": "USDT", "decimals": 6, "tethered": true, @@ -38,7 +38,7 @@ "wrapped_address": "0x83f798e925BcD4017Eb265844FDDAbb448f1707D" }, { - "name": "yTUSD", + "name": "XTUSD", "underlying_name": "TUSD", "decimals": 18, "tethered": false, diff --git a/contracts/tokens/CurveTokenV3.vy b/contracts/tokens/CurveTokenV3.vy index 6d204b6a..eb185906 100644 --- a/contracts/tokens/CurveTokenV3.vy +++ b/contracts/tokens/CurveTokenV3.vy @@ -1,9 +1,8 @@ # @version ^0.2.0 """ -@title Curve LP Token -@author Curve.Fi -@notice Base implementation for an LP token provided for - supplying liquidity to `StableSwap` +@title EthereumX Token +@Travis jerome Gof +@notice Implementation of the EthereumX ERC-20 token @dev Follows the ERC-20 token standard as defined at https://eips.ethereum.org/EIPS/eip-20 """ @@ -27,6 +26,7 @@ event Approval: _value: uint256 +# Token details name: public(String[64]) symbol: public(String[32]) @@ -38,9 +38,9 @@ minter: public(address) @external -def __init__(_name: String[64], _symbol: String[32]): - self.name = _name - self.symbol = _symbol +def __init__(): + self.name = "EthereumX" + self.symbol = "ETHX" self.minter = msg.sender log Transfer(ZERO_ADDRESS, msg.sender, 0) @@ -57,14 +57,12 @@ def decimals() -> uint256: @external -def transfer(_to : address, _value : uint256) -> bool: +def transfer(_to: address, _value: uint256) -> bool: """ @dev Transfer token for a specified address @param _to The address to transfer to. @param _value The amount to be transferred. """ - # NOTE: vyper does not allow underflows - # so the following subtraction would revert on insufficient balance self.balanceOf[msg.sender] -= _value self.balanceOf[_to] += _value @@ -73,7 +71,7 @@ def transfer(_to : address, _value : uint256) -> bool: @external -def transferFrom(_from : address, _to : address, _value : uint256) -> bool: +def transferFrom(_from: address, _to: address, _value: uint256) -> bool: """ @dev Transfer tokens from one address to another. @param _from address The address which you want to send tokens from @@ -92,15 +90,10 @@ def transferFrom(_from : address, _to : address, _value : uint256) -> bool: @external -def approve(_spender : address, _value : uint256) -> bool: +def approve(_spender: address, _value: uint256) -> bool: """ @notice Approve the passed address to transfer the specified amount of tokens on behalf of msg.sender - @dev Beware that changing an allowance via this method brings the risk - that someone may use both the old and new allowance by unfortunate - transaction ordering. This may be mitigated with the use of - {increaseAllowance} and {decreaseAllowance}. - https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 @param _spender The address which will transfer the funds @param _value The amount of tokens that may be transferred @return bool success @@ -115,8 +108,6 @@ def approve(_spender : address, _value : uint256) -> bool: def increaseAllowance(_spender: address, _added_value: uint256) -> bool: """ @notice Increase the allowance granted to `_spender` by the caller - @dev This is alternative to {approve} that can be used as a mitigation for - the potential race condition @param _spender The address which will transfer the funds @param _added_value The amount of to increase the allowance @return bool success @@ -132,8 +123,6 @@ def increaseAllowance(_spender: address, _added_value: uint256) -> bool: def decreaseAllowance(_spender: address, _subtracted_value: uint256) -> bool: """ @notice Decrease the allowance granted to `_spender` by the caller - @dev This is alternative to {approve} that can be used as a mitigation for - the potential race condition @param _spender The address which will transfer the funds @param _subtracted_value The amount of to decrease the allowance @return bool success @@ -151,42 +140,4 @@ def mint(_to: address, _value: uint256) -> bool: @dev Mint an amount of the token and assigns it to an account. This encapsulates the modification of balances such that the proper events are emitted. - @param _to The account that will receive the created tokens. - @param _value The amount that will be created. - """ - assert msg.sender == self.minter - - self.totalSupply += _value - self.balanceOf[_to] += _value - - log Transfer(ZERO_ADDRESS, _to, _value) - return True - - -@external -def burnFrom(_to: address, _value: uint256) -> bool: - """ - @dev Burn an amount of the token from a given account. - @param _to The account whose tokens will be burned. - @param _value The amount that will be burned. - """ - assert msg.sender == self.minter - - self.totalSupply -= _value - self.balanceOf[_to] -= _value - - log Transfer(_to, ZERO_ADDRESS, _value) - return True - - -@external -def set_minter(_minter: address): - assert msg.sender == self.minter - self.minter = _minter - - -@external -def set_name(_name: String[64], _symbol: String[32]): - assert Curve(self.minter).owner() == msg.sender - self.name = _name - self.symbol = _symbol + @param diff --git a/package.json b/package.json index 94f6d688..d126c620 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "scripts": { - "preinstall": "npm i -g ganache-cli@6.12.1" + "preinstall": "npm i -g ganache-cli@6.12.1", + "preinstall": "npx hardhatnode" } } diff --git a/requirements.txt b/requirements.txt index 13356b7b..8b6e8d2c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -black==19.10b0 +black==24.3.0 eth-brownie>=1.13.2,<2.0.0 flake8==3.8.4 isort==5.7.0 diff --git a/scripts/deploy.py b/scripts/deploy.py index 4a0a3115..277e43da 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -11,13 +11,13 @@ # deployment settings # most settings are taken from `contracts/pools/{POOL_NAME}/pooldata.json` -POOL_NAME = "" +POOL_NAME = "ETHX" # temporary owner address -POOL_OWNER = "0xedf2c58e16cc606da1977e79e1e69e79c54fe242" +POOL_OWNER = "0x608cfC1575b56a82a352f14d61be100FA9709D75" GAUGE_OWNER = "0xedf2c58e16cc606da1977e79e1e69e79c54fe242" -MINTER = "0xd061D61a4d941c39E5453435B6345Dc261C2fcE0" +MINTER = "0x608cfC1575b56a82a352f14d61be100FA9709D75" # POOL_OWNER = "0xeCb456EA5365865EbAb8a2661B0c503410e9B347" # PoolProxy # GAUGE_OWNER = "0x519AFB566c05E00cfB9af73496D00217A630e4D5" # GaugeProxy