diff --git a/contracts/contract/BaseUpgradeable.sol b/contracts/contract/BaseUpgradeable.sol index b4bcf9d..6652f98 100644 --- a/contracts/contract/BaseUpgradeable.sol +++ b/contracts/contract/BaseUpgradeable.sol @@ -10,4 +10,8 @@ contract BaseUpgradeable is Initializable, BaseAbstract { function __BaseUpgradeable_init(Storage gogoStorageAddress) internal onlyInitializing { gogoStorage = Storage(gogoStorageAddress); } + + /// @dev This empty reserved space is put in place to allow future versions to add new + /// variables without shifting down storage in the inheritance chain. + uint256[50] private __gap; } diff --git a/contracts/contract/tokens/upgradeable/ERC20Upgradeable.sol b/contracts/contract/tokens/upgradeable/ERC20Upgradeable.sol index c67f057..5913c1b 100644 --- a/contracts/contract/tokens/upgradeable/ERC20Upgradeable.sol +++ b/contracts/contract/tokens/upgradeable/ERC20Upgradeable.sol @@ -205,4 +205,8 @@ abstract contract ERC20Upgradeable is Initializable { emit Transfer(from, address(0), amount); } + + /// @dev This empty reserved space is put in place to allow future versions to add new + /// variables without shifting down storage in the inheritance chain. + uint256[50] private __gap; } diff --git a/contracts/contract/tokens/upgradeable/ERC4626Upgradeable.sol b/contracts/contract/tokens/upgradeable/ERC4626Upgradeable.sol index 23e8bc8..c39e7f8 100644 --- a/contracts/contract/tokens/upgradeable/ERC4626Upgradeable.sol +++ b/contracts/contract/tokens/upgradeable/ERC4626Upgradeable.sol @@ -176,4 +176,8 @@ abstract contract ERC4626Upgradeable is Initializable, ERC20Upgradeable { function beforeWithdraw(uint256 assets, uint256 shares) internal virtual {} function afterDeposit(uint256 assets, uint256 shares) internal virtual {} + + /// @dev This empty reserved space is put in place to allow future versions to add new + /// variables without shifting down storage in the inheritance chain. + uint256[50] private __gap; } diff --git a/test/unit/TokenUpgradeTests.t.sol b/test/unit/TokenUpgradeTests.t.sol index b5805e8..9e2c9e5 100644 --- a/test/unit/TokenUpgradeTests.t.sol +++ b/test/unit/TokenUpgradeTests.t.sol @@ -5,6 +5,8 @@ pragma solidity 0.8.17; import "./utils/BaseTest.sol"; import {MockTokenggAVAXV2} from "./utils/MockTokenggAVAXV2.sol"; +import {MockTokenggAVAXV2Dangerous} from "./utils/MockTokenggAVAXV2Dangerous.sol"; +import {MockTokenggAVAXV2Safe} from "./utils/MockTokenggAVAXV2Safe.sol"; contract TokenUpgradeTests is BaseTest { function setUp() public override { @@ -33,4 +35,59 @@ contract TokenUpgradeTests is BaseTest { assertEq(address(proxy), oldAddress); assertEq(proxy.name(), oldName); } + + function testStorageGapDangerouslySet() public { + // initialize token + TokenggAVAX impl = new TokenggAVAX(); + TokenggAVAX proxy = TokenggAVAX(deployProxy(address(impl), guardian)); + + proxy.initialize(store, wavax); + + proxy.syncRewards(); + vm.warp(ggAVAX.rewardsCycleEnd()); + + // add some rewards to make sure error error occurs + address alice = getActorWithTokens("alice", 1000 ether, 0 ether); + vm.prank(alice); + wavax.transfer(address(proxy), 1000 ether); + proxy.syncRewards(); + + uint256 oldLastSync = proxy.lastSync(); + bytes32 oldDomainSeparator = proxy.DOMAIN_SEPARATOR(); + + // upgrade implementation + MockTokenggAVAXV2Dangerous impl2 = new MockTokenggAVAXV2Dangerous(); + vm.prank(guardian); + proxy.upgradeTo(address(impl2)); + proxy.initialize(store, wavax); + + // now lastSync is reading four bytes of lastRewardsAmt + assertFalse(proxy.lastSync() == oldLastSync); + + // domain separator also does not change but should during regular upgrade + assertEq(proxy.DOMAIN_SEPARATOR(), oldDomainSeparator); + } + + function testStorageGapSafe() public { + // initialize token + TokenggAVAX impl = new TokenggAVAX(); + TokenggAVAX proxy = TokenggAVAX(deployProxy(address(impl), guardian)); + + proxy.initialize(store, wavax); + + proxy.syncRewards(); + uint256 oldLastSync = proxy.lastSync(); + bytes32 oldDomainSeparator = proxy.DOMAIN_SEPARATOR(); + + // upgrade implementation + MockTokenggAVAXV2Safe impl2 = new MockTokenggAVAXV2Safe(); + vm.prank(guardian); + proxy.upgradeTo(address(impl2)); + proxy.initialize(store, wavax); + + // verify that lastSync is not overwritten during upgrade + assertEq(proxy.lastSync(), oldLastSync); + // verify domain separator changes + assertFalse(proxy.DOMAIN_SEPARATOR() == oldDomainSeparator); + } } diff --git a/test/unit/utils/ERC20UpgradeableDangerous.sol b/test/unit/utils/ERC20UpgradeableDangerous.sol new file mode 100644 index 0000000..db0b239 --- /dev/null +++ b/test/unit/utils/ERC20UpgradeableDangerous.sol @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +/// @notice Modern and gas efficient ERC20 + EIP-2612 implementation. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC20.sol) +/// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol) +/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it. +abstract contract ERC20UpgradeableDangerous is Initializable { + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event Transfer(address indexed from, address indexed to, uint256 amount); + + event Approval(address indexed owner, address indexed spender, uint256 amount); + + /*////////////////////////////////////////////////////////////// + METADATA STORAGE + //////////////////////////////////////////////////////////////*/ + + string public name; + + string public symbol; + + uint8 public decimals; + + /*////////////////////////////////////////////////////////////// + ERC20 STORAGE + //////////////////////////////////////////////////////////////*/ + + uint256 public totalSupply; + + mapping(address => uint256) public balanceOf; + + mapping(address => mapping(address => uint256)) public allowance; + + /*////////////////////////////////////////////////////////////// + EIP-2612 STORAGE + //////////////////////////////////////////////////////////////*/ + + uint256 internal INITIAL_CHAIN_ID; + + bytes32 internal INITIAL_DOMAIN_SEPARATOR; + + mapping(address => uint256) public nonces; + + // New storage variable that we do NOT account for in the gap + uint256 public dangerousVariable; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + function __ERC20Upgradeable_init( + string memory _name, + string memory _symbol, + uint8 _decimals + ) internal onlyInitializing { + name = _name; + symbol = _symbol; + decimals = _decimals; + + INITIAL_CHAIN_ID = block.chainid; + INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); + } + + /*////////////////////////////////////////////////////////////// + ERC20 LOGIC + //////////////////////////////////////////////////////////////*/ + + function approve(address spender, uint256 amount) public virtual returns (bool) { + allowance[msg.sender][spender] = amount; + + emit Approval(msg.sender, spender, amount); + + return true; + } + + function transfer(address to, uint256 amount) public virtual returns (bool) { + balanceOf[msg.sender] -= amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; + } + + emit Transfer(msg.sender, to, amount); + + return true; + } + + function transferFrom( + address from, + address to, + uint256 amount + ) public virtual returns (bool) { + uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. + + if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; + + balanceOf[from] -= amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; + } + + emit Transfer(from, to, amount); + + return true; + } + + /*////////////////////////////////////////////////////////////// + EIP-2612 LOGIC + //////////////////////////////////////////////////////////////*/ + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED"); + + // Unchecked because the only math done is incrementing + // the owner's nonce which cannot realistically overflow. + unchecked { + address recoveredAddress = ecrecover( + keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + owner, + spender, + value, + nonces[owner]++, + deadline + ) + ) + ) + ), + v, + r, + s + ); + + require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER"); + + allowance[recoveredAddress][spender] = value; + } + + emit Approval(owner, spender, value); + } + + function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { + return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator(); + } + + function computeDomainSeparator() internal view virtual returns (bytes32) { + return + keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(name)), + versionHash(), + block.chainid, + address(this) + ) + ); + } + + function versionHash() internal view virtual returns (bytes32); + + /*////////////////////////////////////////////////////////////// + INTERNAL MINT/BURN LOGIC + //////////////////////////////////////////////////////////////*/ + + function _mint(address to, uint256 amount) internal virtual { + totalSupply += amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; + } + + emit Transfer(address(0), to, amount); + } + + function _burn(address from, uint256 amount) internal virtual { + balanceOf[from] -= amount; + + // Cannot underflow because a user's balance + // will never be larger than the total supply. + unchecked { + totalSupply -= amount; + } + + emit Transfer(from, address(0), amount); + } + + /// @dev This empty reserved space is put in place to allow future versions to add new + /// variables without shifting down storage in the inheritance chain. + uint256[50] private __gap; +} diff --git a/test/unit/utils/ERC20UpgradeableSafe.sol b/test/unit/utils/ERC20UpgradeableSafe.sol new file mode 100644 index 0000000..cfcc79d --- /dev/null +++ b/test/unit/utils/ERC20UpgradeableSafe.sol @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +/// @notice Modern and gas efficient ERC20 + EIP-2612 implementation. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC20.sol) +/// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol) +/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it. +abstract contract ERC20UpgradeableSafe is Initializable { + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event Transfer(address indexed from, address indexed to, uint256 amount); + + event Approval(address indexed owner, address indexed spender, uint256 amount); + + /*////////////////////////////////////////////////////////////// + METADATA STORAGE + //////////////////////////////////////////////////////////////*/ + + string public name; + + string public symbol; + + uint8 public decimals; + + /*////////////////////////////////////////////////////////////// + ERC20 STORAGE + //////////////////////////////////////////////////////////////*/ + + uint256 public totalSupply; + + mapping(address => uint256) public balanceOf; + + mapping(address => mapping(address => uint256)) public allowance; + + /*////////////////////////////////////////////////////////////// + EIP-2612 STORAGE + //////////////////////////////////////////////////////////////*/ + + uint256 internal INITIAL_CHAIN_ID; + + bytes32 internal INITIAL_DOMAIN_SEPARATOR; + + mapping(address => uint256) public nonces; + + // New storage variable that we DO account for in the gap + uint256 public dangerousVariable; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + function __ERC20Upgradeable_init( + string memory _name, + string memory _symbol, + uint8 _decimals + ) internal onlyInitializing { + name = _name; + symbol = _symbol; + decimals = _decimals; + // dangerousVariable = 1234; + + INITIAL_CHAIN_ID = block.chainid; + INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); + } + + /*////////////////////////////////////////////////////////////// + ERC20 LOGIC + //////////////////////////////////////////////////////////////*/ + + function approve(address spender, uint256 amount) public virtual returns (bool) { + allowance[msg.sender][spender] = amount; + + emit Approval(msg.sender, spender, amount); + + return true; + } + + function transfer(address to, uint256 amount) public virtual returns (bool) { + balanceOf[msg.sender] -= amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; + } + + emit Transfer(msg.sender, to, amount); + + return true; + } + + function transferFrom( + address from, + address to, + uint256 amount + ) public virtual returns (bool) { + uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. + + if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; + + balanceOf[from] -= amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; + } + + emit Transfer(from, to, amount); + + return true; + } + + /*////////////////////////////////////////////////////////////// + EIP-2612 LOGIC + //////////////////////////////////////////////////////////////*/ + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED"); + + // Unchecked because the only math done is incrementing + // the owner's nonce which cannot realistically overflow. + unchecked { + address recoveredAddress = ecrecover( + keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + owner, + spender, + value, + nonces[owner]++, + deadline + ) + ) + ) + ), + v, + r, + s + ); + + require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER"); + + allowance[recoveredAddress][spender] = value; + } + + emit Approval(owner, spender, value); + } + + function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { + return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator(); + } + + function computeDomainSeparator() internal view virtual returns (bytes32) { + return + keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(name)), + versionHash(), + block.chainid, + address(this) + ) + ); + } + + function versionHash() internal view virtual returns (bytes32); + + /*////////////////////////////////////////////////////////////// + INTERNAL MINT/BURN LOGIC + //////////////////////////////////////////////////////////////*/ + + function _mint(address to, uint256 amount) internal virtual { + totalSupply += amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; + } + + emit Transfer(address(0), to, amount); + } + + function _burn(address from, uint256 amount) internal virtual { + balanceOf[from] -= amount; + + // Cannot underflow because a user's balance + // will never be larger than the total supply. + unchecked { + totalSupply -= amount; + } + + emit Transfer(from, address(0), amount); + } + + /// @dev This empty reserved space is put in place to allow future versions to add new + /// variables without shifting down storage in the inheritance chain. + uint256[49] private __gap; +} diff --git a/test/unit/utils/ERC4626UpgradeableDangerous.sol b/test/unit/utils/ERC4626UpgradeableDangerous.sol new file mode 100644 index 0000000..1a51b29 --- /dev/null +++ b/test/unit/utils/ERC4626UpgradeableDangerous.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +import {ERC20UpgradeableDangerous} from "./ERC20UpgradeableDangerous.sol"; + +import {ERC20} from "@rari-capital/solmate/src/tokens/ERC20.sol"; +import {FixedPointMathLib} from "@rari-capital/solmate/src/utils/FixedPointMathLib.sol"; +import {SafeTransferLib} from "@rari-capital/solmate/src/utils/SafeTransferLib.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +abstract contract ERC4626UpgradeableDangerous is Initializable, ERC20UpgradeableDangerous { + using SafeTransferLib for ERC20; + using FixedPointMathLib for uint256; + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares); + + event Withdraw(address indexed caller, address indexed receiver, address indexed owner, uint256 assets, uint256 shares); + + /*////////////////////////////////////////////////////////////// + IMMUTABLES + //////////////////////////////////////////////////////////////*/ + + ERC20 public asset; + + function __ERC4626Upgradeable_init( + ERC20 _asset, + string memory _name, + string memory _symbol + ) internal onlyInitializing { + __ERC20Upgradeable_init(_name, _symbol, _asset.decimals()); + asset = _asset; + } + + /*////////////////////////////////////////////////////////////// + DEPOSIT/WITHDRAWAL LOGIC + //////////////////////////////////////////////////////////////*/ + + function deposit(uint256 assets, address receiver) public virtual returns (uint256 shares) { + // Check for rounding error since we round down in previewDeposit. + require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES"); + + // Need to transfer before minting or ERC777s could reenter. + asset.safeTransferFrom(msg.sender, address(this), assets); + + _mint(receiver, shares); + + emit Deposit(msg.sender, receiver, assets, shares); + + afterDeposit(assets, shares); + } + + function mint(uint256 shares, address receiver) public virtual returns (uint256 assets) { + assets = previewMint(shares); // No need to check for rounding error, previewMint rounds up. + + // Need to transfer before minting or ERC777s could reenter. + asset.safeTransferFrom(msg.sender, address(this), assets); + + _mint(receiver, shares); + + emit Deposit(msg.sender, receiver, assets, shares); + + afterDeposit(assets, shares); + } + + function withdraw( + uint256 assets, + address receiver, + address owner + ) public virtual returns (uint256 shares) { + shares = previewWithdraw(assets); // No need to check for rounding error, previewWithdraw rounds up. + + if (msg.sender != owner) { + uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals. + + if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; + } + + beforeWithdraw(assets, shares); + + _burn(owner, shares); + + emit Withdraw(msg.sender, receiver, owner, assets, shares); + + asset.safeTransfer(receiver, assets); + } + + function redeem( + uint256 shares, + address receiver, + address owner + ) public virtual returns (uint256 assets) { + if (msg.sender != owner) { + uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals. + + if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; + } + + // Check for rounding error since we round down in previewRedeem. + require((assets = previewRedeem(shares)) != 0, "ZERO_ASSETS"); + + beforeWithdraw(assets, shares); + + _burn(owner, shares); + + emit Withdraw(msg.sender, receiver, owner, assets, shares); + + asset.safeTransfer(receiver, assets); + } + + /*////////////////////////////////////////////////////////////// + ACCOUNTING LOGIC + //////////////////////////////////////////////////////////////*/ + + function totalAssets() public view virtual returns (uint256); + + function convertToShares(uint256 assets) public view virtual returns (uint256) { + uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + + return supply == 0 ? assets : assets.mulDivDown(supply, totalAssets()); + } + + function convertToAssets(uint256 shares) public view virtual returns (uint256) { + uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + + return supply == 0 ? shares : shares.mulDivDown(totalAssets(), supply); + } + + function previewDeposit(uint256 assets) public view virtual returns (uint256) { + return convertToShares(assets); + } + + function previewMint(uint256 shares) public view virtual returns (uint256) { + uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + + return supply == 0 ? shares : shares.mulDivUp(totalAssets(), supply); + } + + function previewWithdraw(uint256 assets) public view virtual returns (uint256) { + uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + + return supply == 0 ? assets : assets.mulDivUp(supply, totalAssets()); + } + + function previewRedeem(uint256 shares) public view virtual returns (uint256) { + return convertToAssets(shares); + } + + /*////////////////////////////////////////////////////////////// + DEPOSIT/WITHDRAWAL LIMIT LOGIC + //////////////////////////////////////////////////////////////*/ + + function maxDeposit(address) public view virtual returns (uint256) { + return type(uint256).max; + } + + function maxMint(address) public view virtual returns (uint256) { + return type(uint256).max; + } + + function maxWithdraw(address owner) public view virtual returns (uint256) { + return convertToAssets(balanceOf[owner]); + } + + function maxRedeem(address owner) public view virtual returns (uint256) { + return balanceOf[owner]; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL HOOKS LOGIC + //////////////////////////////////////////////////////////////*/ + + function beforeWithdraw(uint256 assets, uint256 shares) internal virtual {} + + function afterDeposit(uint256 assets, uint256 shares) internal virtual {} + + /// @dev This empty reserved space is put in place to allow future versions to add new + /// variables without shifting down storage in the inheritance chain. + uint256[50] private __gap; +} diff --git a/test/unit/utils/ERC4626UpgradeableSafe.sol b/test/unit/utils/ERC4626UpgradeableSafe.sol new file mode 100644 index 0000000..1170126 --- /dev/null +++ b/test/unit/utils/ERC4626UpgradeableSafe.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +import {ERC20UpgradeableSafe} from "./ERC20UpgradeableSafe.sol"; + +import {ERC20} from "@rari-capital/solmate/src/tokens/ERC20.sol"; +import {FixedPointMathLib} from "@rari-capital/solmate/src/utils/FixedPointMathLib.sol"; +import {SafeTransferLib} from "@rari-capital/solmate/src/utils/SafeTransferLib.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +abstract contract ERC4626UpgradeableSafe is Initializable, ERC20UpgradeableSafe { + using SafeTransferLib for ERC20; + using FixedPointMathLib for uint256; + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares); + + event Withdraw(address indexed caller, address indexed receiver, address indexed owner, uint256 assets, uint256 shares); + + /*////////////////////////////////////////////////////////////// + IMMUTABLES + //////////////////////////////////////////////////////////////*/ + + ERC20 public asset; + + function __ERC4626Upgradeable_init( + ERC20 _asset, + string memory _name, + string memory _symbol + ) internal onlyInitializing { + __ERC20Upgradeable_init(_name, _symbol, _asset.decimals()); + asset = _asset; + } + + /*////////////////////////////////////////////////////////////// + DEPOSIT/WITHDRAWAL LOGIC + //////////////////////////////////////////////////////////////*/ + + function deposit(uint256 assets, address receiver) public virtual returns (uint256 shares) { + // Check for rounding error since we round down in previewDeposit. + require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES"); + + // Need to transfer before minting or ERC777s could reenter. + asset.safeTransferFrom(msg.sender, address(this), assets); + + _mint(receiver, shares); + + emit Deposit(msg.sender, receiver, assets, shares); + + afterDeposit(assets, shares); + } + + function mint(uint256 shares, address receiver) public virtual returns (uint256 assets) { + assets = previewMint(shares); // No need to check for rounding error, previewMint rounds up. + + // Need to transfer before minting or ERC777s could reenter. + asset.safeTransferFrom(msg.sender, address(this), assets); + + _mint(receiver, shares); + + emit Deposit(msg.sender, receiver, assets, shares); + + afterDeposit(assets, shares); + } + + function withdraw( + uint256 assets, + address receiver, + address owner + ) public virtual returns (uint256 shares) { + shares = previewWithdraw(assets); // No need to check for rounding error, previewWithdraw rounds up. + + if (msg.sender != owner) { + uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals. + + if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; + } + + beforeWithdraw(assets, shares); + + _burn(owner, shares); + + emit Withdraw(msg.sender, receiver, owner, assets, shares); + + asset.safeTransfer(receiver, assets); + } + + function redeem( + uint256 shares, + address receiver, + address owner + ) public virtual returns (uint256 assets) { + if (msg.sender != owner) { + uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals. + + if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; + } + + // Check for rounding error since we round down in previewRedeem. + require((assets = previewRedeem(shares)) != 0, "ZERO_ASSETS"); + + beforeWithdraw(assets, shares); + + _burn(owner, shares); + + emit Withdraw(msg.sender, receiver, owner, assets, shares); + + asset.safeTransfer(receiver, assets); + } + + /*////////////////////////////////////////////////////////////// + ACCOUNTING LOGIC + //////////////////////////////////////////////////////////////*/ + + function totalAssets() public view virtual returns (uint256); + + function convertToShares(uint256 assets) public view virtual returns (uint256) { + uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + + return supply == 0 ? assets : assets.mulDivDown(supply, totalAssets()); + } + + function convertToAssets(uint256 shares) public view virtual returns (uint256) { + uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + + return supply == 0 ? shares : shares.mulDivDown(totalAssets(), supply); + } + + function previewDeposit(uint256 assets) public view virtual returns (uint256) { + return convertToShares(assets); + } + + function previewMint(uint256 shares) public view virtual returns (uint256) { + uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + + return supply == 0 ? shares : shares.mulDivUp(totalAssets(), supply); + } + + function previewWithdraw(uint256 assets) public view virtual returns (uint256) { + uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + + return supply == 0 ? assets : assets.mulDivUp(supply, totalAssets()); + } + + function previewRedeem(uint256 shares) public view virtual returns (uint256) { + return convertToAssets(shares); + } + + /*////////////////////////////////////////////////////////////// + DEPOSIT/WITHDRAWAL LIMIT LOGIC + //////////////////////////////////////////////////////////////*/ + + function maxDeposit(address) public view virtual returns (uint256) { + return type(uint256).max; + } + + function maxMint(address) public view virtual returns (uint256) { + return type(uint256).max; + } + + function maxWithdraw(address owner) public view virtual returns (uint256) { + return convertToAssets(balanceOf[owner]); + } + + function maxRedeem(address owner) public view virtual returns (uint256) { + return balanceOf[owner]; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL HOOKS LOGIC + //////////////////////////////////////////////////////////////*/ + + function beforeWithdraw(uint256 assets, uint256 shares) internal virtual {} + + function afterDeposit(uint256 assets, uint256 shares) internal virtual {} + + /// @dev This empty reserved space is put in place to allow future versions to add new + /// variables without shifting down storage in the inheritance chain. + uint256[50] private __gap; +} diff --git a/test/unit/utils/MockTokenggAVAXV2Dangerous.sol b/test/unit/utils/MockTokenggAVAXV2Dangerous.sol new file mode 100644 index 0000000..ea9f10d --- /dev/null +++ b/test/unit/utils/MockTokenggAVAXV2Dangerous.sol @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Copied from https://github.com/fei-protocol/ERC4626/blob/main/src/xERC4626.sol +// Rewards logic inspired by xERC20 (https://github.com/ZeframLou/playpen/blob/main/src/xERC20.sol) +pragma solidity 0.8.17; + +import "../../../contracts/contract/BaseUpgradeable.sol"; +import {ERC20Upgradeable} from "../../../contracts/contract/tokens/upgradeable/ERC20Upgradeable.sol"; +import {ERC20UpgradeableDangerous} from "./ERC20UpgradeableDangerous.sol"; +import {ERC4626UpgradeableDangerous} from "./ERC4626UpgradeableDangerous.sol"; +import {ProtocolDAO} from "../../../contracts/contract/ProtocolDAO.sol"; +import {Storage} from "../../../contracts/contract/Storage.sol"; + +import {IWithdrawer} from "../../../contracts/interface/IWithdrawer.sol"; +import {IWAVAX} from "../../../contracts/interface/IWAVAX.sol"; + +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +import {ERC20} from "@rari-capital/solmate/src/mixins/ERC4626.sol"; +import {FixedPointMathLib} from "@rari-capital/solmate/src/utils/FixedPointMathLib.sol"; +import {SafeCastLib} from "@rari-capital/solmate/src/utils/SafeCastLib.sol"; +import {SafeTransferLib} from "@rari-capital/solmate/src/utils/SafeTransferLib.sol"; +import {console} from "forge-std/console.sol"; + +/// @dev Local variables and parent contracts must remain in order between contract upgrades +contract MockTokenggAVAXV2Dangerous is Initializable, ERC4626UpgradeableDangerous, UUPSUpgradeable, BaseUpgradeable { + using SafeTransferLib for ERC20; + using SafeTransferLib for address; + using SafeCastLib for *; + using FixedPointMathLib for uint256; + + error SyncError(); + error ZeroShares(); + error ZeroAssets(); + error InvalidStakingDeposit(); + error WithdrawAmountTooLarge(); + + event NewRewardsCycle(uint256 indexed cycleEnd, uint256 rewardsAmt); + event WithdrawnForStaking(address indexed caller, uint256 assets); + event DepositedFromStaking(address indexed caller, uint256 baseAmt, uint256 rewardsAmt); + + /// @notice the effective start of the current cycle + uint32 public lastSync; + + /// @notice the maximum length of a rewards cycle + uint32 public rewardsCycleLength; + + /// @notice the end of the current cycle. Will always be evenly divisible by `rewardsCycleLength`. + uint32 public rewardsCycleEnd; + + /// @notice the amount of rewards distributed in a the most recent cycle. + uint192 public lastRewardsAmt; + + /// @notice the total amount of avax (including avax sent out for staking and all incoming rewards) + uint256 public totalReleasedAssets; + + /// @notice total amount of avax currently out for staking (not including any rewards) + uint256 public stakingTotalAssets; + + modifier whenTokenNotPaused(uint256 amt) { + if (amt > 0 && getBool(keccak256(abi.encodePacked("contract.paused", "TokenggAVAX")))) { + revert ContractPaused(); + } + _; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + // The constructor is exectued only when creating implementation contract + // so prevent it's reinitialization + _disableInitializers(); + } + + function initialize(Storage storageAddress, ERC20 asset) public reinitializer(2) { + __ERC4626Upgradeable_init(asset, "GoGoPool Liquid Staking Token", "ggAVAXv2"); + __BaseUpgradeable_init(storageAddress); + + version = 2; + + rewardsCycleLength = 14 days; + // Ensure it will be evenly divisible by `rewardsCycleLength`. + rewardsCycleEnd = (block.timestamp.safeCastTo32() / rewardsCycleLength) * rewardsCycleLength; + } + + /// @notice only accept AVAX via fallback from the WAVAX contract + receive() external payable { + assert(msg.sender == address(asset)); + } + + /// @notice Distributes rewards to TokenggAVAX holders. Public, anyone can call. + /// All surplus `asset` balance of the contract over the internal balance becomes queued for the next cycle. + function syncRewards() public { + uint32 timestamp = block.timestamp.safeCastTo32(); + + if (timestamp < rewardsCycleEnd) { + revert SyncError(); + } + + uint192 lastRewardsAmt_ = lastRewardsAmt; + uint256 totalReleasedAssets_ = totalReleasedAssets; + uint256 stakingTotalAssets_ = stakingTotalAssets; + + uint256 nextRewardsAmt = (asset.balanceOf(address(this)) + stakingTotalAssets_) - totalReleasedAssets_ - lastRewardsAmt_; + + // Ensure nextRewardsCycleEnd will be evenly divisible by `rewardsCycleLength`. + uint32 nextRewardsCycleEnd = ((timestamp + rewardsCycleLength) / rewardsCycleLength) * rewardsCycleLength; + + lastRewardsAmt = nextRewardsAmt.safeCastTo192(); + lastSync = timestamp; + rewardsCycleEnd = nextRewardsCycleEnd; + totalReleasedAssets = totalReleasedAssets_ + lastRewardsAmt_; + emit NewRewardsCycle(nextRewardsCycleEnd, nextRewardsAmt); + } + + /// @notice Compute the amount of tokens available to share holders. + /// Increases linearly during a reward distribution period from the sync call, not the cycle start. + function totalAssets() public view override returns (uint256) { + // cache global vars + uint256 totalReleasedAssets_ = totalReleasedAssets; + uint192 lastRewardsAmt_ = lastRewardsAmt; + uint32 rewardsCycleEnd_ = rewardsCycleEnd; + uint32 lastSync_ = lastSync; + + if (block.timestamp >= rewardsCycleEnd_) { + // no rewards or rewards are fully unlocked + // entire reward amount is available + return totalReleasedAssets_ + lastRewardsAmt_; + } + + // rewards are not fully unlocked + // return unlocked rewards and stored total + uint256 unlockedRewards = (lastRewardsAmt_ * (block.timestamp - lastSync_)) / (rewardsCycleEnd_ - lastSync_); + return totalReleasedAssets_ + unlockedRewards; + } + + /// @notice Returns the AVAX amount that is available for staking on minipools + /// @return uint256 AVAX available for staking + function amountAvailableForStaking() public view returns (uint256) { + ProtocolDAO protocolDAO = ProtocolDAO(getContractAddress("ProtocolDAO")); + uint256 targetCollateralRate = protocolDAO.getTargetGGAVAXReserveRate(); + + uint256 totalAssets_ = totalAssets(); + + uint256 reservedAssets = totalAssets_.mulDivDown(targetCollateralRate, 1 ether); + return totalAssets_ - reservedAssets - stakingTotalAssets; + } + + /// @notice Accepts AVAX deposit from a minipool. Expects the base amount and rewards earned from staking + /// @param baseAmt The amount of liquid staker AVAX used to create a minipool + /// @param rewardAmt The rewards amount (in AVAX) earned from staking + function depositFromStaking(uint256 baseAmt, uint256 rewardAmt) public payable onlySpecificRegisteredContract("MinipoolManager", msg.sender) { + uint256 totalAmt = msg.value; + if (totalAmt != (baseAmt + rewardAmt) || baseAmt > stakingTotalAssets) { + revert InvalidStakingDeposit(); + } + + emit DepositedFromStaking(msg.sender, baseAmt, rewardAmt); + stakingTotalAssets -= baseAmt; + IWAVAX(address(asset)).deposit{value: totalAmt}(); + } + + /// @notice Allows the MinipoolManager contract to withdraw liquid staker funds to create a minipool + /// @param assets The amount of AVAX to withdraw + function withdrawForStaking(uint256 assets) public onlySpecificRegisteredContract("MinipoolManager", msg.sender) { + if (assets > amountAvailableForStaking()) { + revert WithdrawAmountTooLarge(); + } + + emit WithdrawnForStaking(msg.sender, assets); + + stakingTotalAssets += assets; + IWAVAX(address(asset)).withdraw(assets); + IWithdrawer withdrawer = IWithdrawer(msg.sender); + withdrawer.receiveWithdrawalAVAX{value: assets}(); + } + + /// @notice Allows users to deposit AVAX and recieve ggAVAX + /// @return shares The amount of ggAVAX minted + function depositAVAX() public payable returns (uint256 shares) { + uint256 assets = msg.value; + // Check for rounding error since we round down in previewDeposit. + if ((shares = previewDeposit(assets)) == 0) { + revert ZeroShares(); + } + + emit Deposit(msg.sender, msg.sender, assets, shares); + + IWAVAX(address(asset)).deposit{value: assets}(); + _mint(msg.sender, shares); + afterDeposit(assets, shares); + } + + /// @notice Allows users to specify an amount of AVAX to withdraw from their ggAVAX supply + /// @param assets Amount of AVAX to be withdrawn + /// @return shares Amount of ggAVAX burned + function withdrawAVAX(uint256 assets) public returns (uint256 shares) { + shares = previewWithdraw(assets); // No need to check for rounding error, previewWithdraw rounds up. + beforeWithdraw(assets, shares); + _burn(msg.sender, shares); + + emit Withdraw(msg.sender, msg.sender, msg.sender, assets, shares); + + IWAVAX(address(asset)).withdraw(assets); + msg.sender.safeTransferETH(assets); + } + + /// @notice Allows users to specify shares of ggAVAX to redeem for AVAX + /// @param shares Amount of ggAVAX to burn + /// @return assets Amount of AVAX withdrawn + function redeemAVAX(uint256 shares) public returns (uint256 assets) { + // Check for rounding error since we round down in previewRedeem. + if ((assets = previewRedeem(shares)) == 0) { + revert ZeroAssets(); + } + beforeWithdraw(assets, shares); + _burn(msg.sender, shares); + + emit Withdraw(msg.sender, msg.sender, msg.sender, assets, shares); + + IWAVAX(address(asset)).withdraw(assets); + msg.sender.safeTransferETH(assets); + } + + /// @notice Max assets an owner can withdraw with consideration to liquidity in this contract + /// @param _owner User wallet address + function maxWithdraw(address _owner) public view override returns (uint256) { + uint256 assets = convertToAssets(balanceOf[_owner]); + uint256 avail = totalAssets() - stakingTotalAssets; + return assets > avail ? avail : assets; + } + + /// @notice Max shares owner can withdraw with consideration to liquidity in this contract + /// @param _owner User wallet address + function maxRedeem(address _owner) public view override returns (uint256) { + uint256 shares = balanceOf[_owner]; + uint256 avail = convertToShares(totalAssets() - stakingTotalAssets); + return shares > avail ? avail : shares; + } + + /// @notice Preview shares minted for AVAX deposit + /// @param assets Amount of AVAX to deposit + /// @return uint256 Amount of ggAVAX that would be minted + function previewDeposit(uint256 assets) public view override whenTokenNotPaused(assets) returns (uint256) { + return super.previewDeposit(assets); + } + + /// @notice Preview assets required for mint of shares + /// @param shares Amount of ggAVAX to mint + /// @return uint256 Amount of AVAX required + function previewMint(uint256 shares) public view override whenTokenNotPaused(shares) returns (uint256) { + return super.previewMint(shares); + } + + /// @notice Function prior to a withdraw + /// @param amount Amount of AVAX + function beforeWithdraw( + uint256 amount, + uint256 /* shares */ + ) internal override { + totalReleasedAssets -= amount; + } + + /// @notice Function after a deposit + /// @param amount Amount of AVAX + function afterDeposit( + uint256 amount, + uint256 /* shares */ + ) internal override { + totalReleasedAssets += amount; + } + + /// @notice Will revert if msg.sender is not authorized to upgrade the contract + function _authorizeUpgrade(address newImplementation) internal override onlyGuardian {} + + function versionHash() internal view override returns (bytes32) { + return keccak256(abi.encodePacked(version)); + } +} diff --git a/test/unit/utils/MockTokenggAVAXV2Safe.sol b/test/unit/utils/MockTokenggAVAXV2Safe.sol new file mode 100644 index 0000000..d4927fa --- /dev/null +++ b/test/unit/utils/MockTokenggAVAXV2Safe.sol @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Copied from https://github.com/fei-protocol/ERC4626/blob/main/src/xERC4626.sol +// Rewards logic inspired by xERC20 (https://github.com/ZeframLou/playpen/blob/main/src/xERC20.sol) +pragma solidity 0.8.17; + +import "../../../contracts/contract/BaseUpgradeable.sol"; +import {ERC4626UpgradeableSafe} from "./ERC4626UpgradeableSafe.sol"; +import {ProtocolDAO} from "../../../contracts/contract/ProtocolDAO.sol"; +import {Storage} from "../../../contracts/contract/Storage.sol"; + +import {IWithdrawer} from "../../../contracts/interface/IWithdrawer.sol"; +import {IWAVAX} from "../../../contracts/interface/IWAVAX.sol"; + +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +import {ERC20} from "@rari-capital/solmate/src/mixins/ERC4626.sol"; +import {FixedPointMathLib} from "@rari-capital/solmate/src/utils/FixedPointMathLib.sol"; +import {SafeCastLib} from "@rari-capital/solmate/src/utils/SafeCastLib.sol"; +import {SafeTransferLib} from "@rari-capital/solmate/src/utils/SafeTransferLib.sol"; +import {console} from "forge-std/console.sol"; + +/// @dev Local variables and parent contracts must remain in order between contract upgrades +contract MockTokenggAVAXV2Safe is Initializable, ERC4626UpgradeableSafe, UUPSUpgradeable, BaseUpgradeable { + using SafeTransferLib for ERC20; + using SafeTransferLib for address; + using SafeCastLib for *; + using FixedPointMathLib for uint256; + + error SyncError(); + error ZeroShares(); + error ZeroAssets(); + error InvalidStakingDeposit(); + error WithdrawAmountTooLarge(); + + event NewRewardsCycle(uint256 indexed cycleEnd, uint256 rewardsAmt); + event WithdrawnForStaking(address indexed caller, uint256 assets); + event DepositedFromStaking(address indexed caller, uint256 baseAmt, uint256 rewardsAmt); + + /// @notice the effective start of the current cycle + uint32 public lastSync; + + /// @notice the maximum length of a rewards cycle + uint32 public rewardsCycleLength; + + /// @notice the end of the current cycle. Will always be evenly divisible by `rewardsCycleLength`. + uint32 public rewardsCycleEnd; + + /// @notice the amount of rewards distributed in a the most recent cycle. + uint192 public lastRewardsAmt; + + /// @notice the total amount of avax (including avax sent out for staking and all incoming rewards) + uint256 public totalReleasedAssets; + + /// @notice total amount of avax currently out for staking (not including any rewards) + uint256 public stakingTotalAssets; + + modifier whenTokenNotPaused(uint256 amt) { + if (amt > 0 && getBool(keccak256(abi.encodePacked("contract.paused", "TokenggAVAX")))) { + revert ContractPaused(); + } + _; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + // The constructor is exectued only when creating implementation contract + // so prevent it's reinitialization + _disableInitializers(); + } + + function initialize(Storage storageAddress, ERC20 asset) public reinitializer(2) { + __ERC4626Upgradeable_init(asset, "GoGoPool Liquid Staking Token", "ggAVAXv2"); + __BaseUpgradeable_init(storageAddress); + + version = 2; + + rewardsCycleLength = 14 days; + // Ensure it will be evenly divisible by `rewardsCycleLength`. + rewardsCycleEnd = (block.timestamp.safeCastTo32() / rewardsCycleLength) * rewardsCycleLength; + } + + /// @notice only accept AVAX via fallback from the WAVAX contract + receive() external payable { + assert(msg.sender == address(asset)); + } + + /// @notice Distributes rewards to TokenggAVAX holders. Public, anyone can call. + /// All surplus `asset` balance of the contract over the internal balance becomes queued for the next cycle. + function syncRewards() public { + uint32 timestamp = block.timestamp.safeCastTo32(); + + if (timestamp < rewardsCycleEnd) { + revert SyncError(); + } + + uint192 lastRewardsAmt_ = lastRewardsAmt; + uint256 totalReleasedAssets_ = totalReleasedAssets; + uint256 stakingTotalAssets_ = stakingTotalAssets; + + uint256 nextRewardsAmt = (asset.balanceOf(address(this)) + stakingTotalAssets_) - totalReleasedAssets_ - lastRewardsAmt_; + + // Ensure nextRewardsCycleEnd will be evenly divisible by `rewardsCycleLength`. + uint32 nextRewardsCycleEnd = ((timestamp + rewardsCycleLength) / rewardsCycleLength) * rewardsCycleLength; + + lastRewardsAmt = nextRewardsAmt.safeCastTo192(); + lastSync = timestamp; + rewardsCycleEnd = nextRewardsCycleEnd; + totalReleasedAssets = totalReleasedAssets_ + lastRewardsAmt_; + emit NewRewardsCycle(nextRewardsCycleEnd, nextRewardsAmt); + } + + /// @notice Compute the amount of tokens available to share holders. + /// Increases linearly during a reward distribution period from the sync call, not the cycle start. + function totalAssets() public view override returns (uint256) { + // cache global vars + uint256 totalReleasedAssets_ = totalReleasedAssets; + uint192 lastRewardsAmt_ = lastRewardsAmt; + uint32 rewardsCycleEnd_ = rewardsCycleEnd; + uint32 lastSync_ = lastSync; + + if (block.timestamp >= rewardsCycleEnd_) { + // no rewards or rewards are fully unlocked + // entire reward amount is available + return totalReleasedAssets_ + lastRewardsAmt_; + } + + // rewards are not fully unlocked + // return unlocked rewards and stored total + uint256 unlockedRewards = (lastRewardsAmt_ * (block.timestamp - lastSync_)) / (rewardsCycleEnd_ - lastSync_); + return totalReleasedAssets_ + unlockedRewards; + } + + /// @notice Returns the AVAX amount that is available for staking on minipools + /// @return uint256 AVAX available for staking + function amountAvailableForStaking() public view returns (uint256) { + ProtocolDAO protocolDAO = ProtocolDAO(getContractAddress("ProtocolDAO")); + uint256 targetCollateralRate = protocolDAO.getTargetGGAVAXReserveRate(); + + uint256 totalAssets_ = totalAssets(); + + uint256 reservedAssets = totalAssets_.mulDivDown(targetCollateralRate, 1 ether); + return totalAssets_ - reservedAssets - stakingTotalAssets; + } + + /// @notice Accepts AVAX deposit from a minipool. Expects the base amount and rewards earned from staking + /// @param baseAmt The amount of liquid staker AVAX used to create a minipool + /// @param rewardAmt The rewards amount (in AVAX) earned from staking + function depositFromStaking(uint256 baseAmt, uint256 rewardAmt) public payable onlySpecificRegisteredContract("MinipoolManager", msg.sender) { + uint256 totalAmt = msg.value; + if (totalAmt != (baseAmt + rewardAmt) || baseAmt > stakingTotalAssets) { + revert InvalidStakingDeposit(); + } + + emit DepositedFromStaking(msg.sender, baseAmt, rewardAmt); + stakingTotalAssets -= baseAmt; + IWAVAX(address(asset)).deposit{value: totalAmt}(); + } + + /// @notice Allows the MinipoolManager contract to withdraw liquid staker funds to create a minipool + /// @param assets The amount of AVAX to withdraw + function withdrawForStaking(uint256 assets) public onlySpecificRegisteredContract("MinipoolManager", msg.sender) { + if (assets > amountAvailableForStaking()) { + revert WithdrawAmountTooLarge(); + } + + emit WithdrawnForStaking(msg.sender, assets); + + stakingTotalAssets += assets; + IWAVAX(address(asset)).withdraw(assets); + IWithdrawer withdrawer = IWithdrawer(msg.sender); + withdrawer.receiveWithdrawalAVAX{value: assets}(); + } + + /// @notice Allows users to deposit AVAX and recieve ggAVAX + /// @return shares The amount of ggAVAX minted + function depositAVAX() public payable returns (uint256 shares) { + uint256 assets = msg.value; + // Check for rounding error since we round down in previewDeposit. + if ((shares = previewDeposit(assets)) == 0) { + revert ZeroShares(); + } + + emit Deposit(msg.sender, msg.sender, assets, shares); + + IWAVAX(address(asset)).deposit{value: assets}(); + _mint(msg.sender, shares); + afterDeposit(assets, shares); + } + + /// @notice Allows users to specify an amount of AVAX to withdraw from their ggAVAX supply + /// @param assets Amount of AVAX to be withdrawn + /// @return shares Amount of ggAVAX burned + function withdrawAVAX(uint256 assets) public returns (uint256 shares) { + shares = previewWithdraw(assets); // No need to check for rounding error, previewWithdraw rounds up. + beforeWithdraw(assets, shares); + _burn(msg.sender, shares); + + emit Withdraw(msg.sender, msg.sender, msg.sender, assets, shares); + + IWAVAX(address(asset)).withdraw(assets); + msg.sender.safeTransferETH(assets); + } + + /// @notice Allows users to specify shares of ggAVAX to redeem for AVAX + /// @param shares Amount of ggAVAX to burn + /// @return assets Amount of AVAX withdrawn + function redeemAVAX(uint256 shares) public returns (uint256 assets) { + // Check for rounding error since we round down in previewRedeem. + if ((assets = previewRedeem(shares)) == 0) { + revert ZeroAssets(); + } + beforeWithdraw(assets, shares); + _burn(msg.sender, shares); + + emit Withdraw(msg.sender, msg.sender, msg.sender, assets, shares); + + IWAVAX(address(asset)).withdraw(assets); + msg.sender.safeTransferETH(assets); + } + + /// @notice Max assets an owner can withdraw with consideration to liquidity in this contract + /// @param _owner User wallet address + function maxWithdraw(address _owner) public view override returns (uint256) { + uint256 assets = convertToAssets(balanceOf[_owner]); + uint256 avail = totalAssets() - stakingTotalAssets; + return assets > avail ? avail : assets; + } + + /// @notice Max shares owner can withdraw with consideration to liquidity in this contract + /// @param _owner User wallet address + function maxRedeem(address _owner) public view override returns (uint256) { + uint256 shares = balanceOf[_owner]; + uint256 avail = convertToShares(totalAssets() - stakingTotalAssets); + return shares > avail ? avail : shares; + } + + /// @notice Preview shares minted for AVAX deposit + /// @param assets Amount of AVAX to deposit + /// @return uint256 Amount of ggAVAX that would be minted + function previewDeposit(uint256 assets) public view override whenTokenNotPaused(assets) returns (uint256) { + return super.previewDeposit(assets); + } + + /// @notice Preview assets required for mint of shares + /// @param shares Amount of ggAVAX to mint + /// @return uint256 Amount of AVAX required + function previewMint(uint256 shares) public view override whenTokenNotPaused(shares) returns (uint256) { + return super.previewMint(shares); + } + + /// @notice Function prior to a withdraw + /// @param amount Amount of AVAX + function beforeWithdraw( + uint256 amount, + uint256 /* shares */ + ) internal override { + totalReleasedAssets -= amount; + } + + /// @notice Function after a deposit + /// @param amount Amount of AVAX + function afterDeposit( + uint256 amount, + uint256 /* shares */ + ) internal override { + totalReleasedAssets += amount; + } + + /// @notice Will revert if msg.sender is not authorized to upgrade the contract + function _authorizeUpgrade(address newImplementation) internal override onlyGuardian {} + + function versionHash() internal view override returns (bytes32) { + return keccak256(abi.encodePacked(version)); + } +}